AI marketing agent for paid ads: run campaigns on 11 ad networks and read your web analytics.
Maintain this server? Claim it
Call this server's tools from your browser. Nothing to install.
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
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.
REQUIRED. Up to 1000.
REQUIRED.
Required ONLY when the ad group is already ENABLED.
ISO 4217 currency code for the bids
Default for entries that do not set one.
This tool isn't marked read-only, so it may change something on the server or in an account you connected.
Pick your client. Values in capitals, like YOUR_TOKEN, are yours to fill in.
Run in a terminal
claude mcp add --transport http hermoso-ads-manager 'https://app.hermoso.ai/mcp?tools=app-ads&src=registry'
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | Required ONLY when the ad group is already ENABLED. |
| currency | string | ISO 4217 currency code for the bids |
| keywords* | array | REQUIRED. Up to 1000. |
| adGroupId* | string | REQUIRED. |
| matchType | string | Default for entries that do not set one. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"keywords"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "Required ONLY when the ad group is already ENABLED."
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code for the bids"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"bid": {
"type": "string",
"description": "decimal bid for this keyword, e.g. 1.50; overrides the ad group default"
},
"text": {
"type": "string",
"description": "keyword text; immutable after creation, delete and re-add to change it"
},
"matchType": {
"type": "string",
"description": "EXACT, BROAD, PHRASE or CATEGORY; overrides the top-level matchType for this keyword"
}
}
},
"description": "REQUIRED. Up to 1000."
},
"adGroupId": {
"type": "string",
"description": "REQUIRED."
},
"matchType": {
"enum": [
"EXACT",
"BROAD",
"PHRASE",
"CATEGORY"
],
"type": "string",
"description": "Default for entries that do not set one."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| keywords* | array | REQUIRED. |
| adGroupId | string | One ad group. Pass this OR campaignId, never both. |
| matchType | string | Default BROAD. |
| campaignId | string | Campaign-wide. Pass this OR adGroupId, never both. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"keywords"
],
"properties": {
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "negative keyword text; immutable, delete and re-add to change it"
},
"matchType": {
"type": "string",
"description": "EXACT or BROAD for this negative keyword; falls back to the top-level matchType"
}
}
},
"description": "REQUIRED."
},
"adGroupId": {
"type": "string",
"description": "One ad group. Pass this OR campaignId, never both."
},
"matchType": {
"enum": [
"EXACT",
"BROAD",
"PHRASE",
"CATEGORY"
],
"type": "string",
"description": "Default BROAD."
},
"campaignId": {
"type": "string",
"description": "Campaign-wide. Pass this OR adGroupId, never both."
}
}
}Add a site to the connected Bing Webmaster account. Important: ADDING IS NOT VERIFYING, and saying so is most of this tool's value: the site arrives UNVERIFIED and every read on it is refused until an ownership proof is placed on the site itself — an XML file at the root, a meta tag in the home page <head>, or a CNAME DNS record — which no API can do. Place one, then call verify_bing_webmaster_site. Microsoft documents that adding a site which is already there does NOT error, so a success here is not even evidence anything changed, which is why the answer is read back from Bing's site list. It is also NOT shared with this profile until the user ticks it under Settings > Connectors > Bing Webmaster Tools > Manage accounts (or call list_connector_accounts with provider bing_webmaster, then set_connector_accounts). 0 credits.
| Argument | Type | Description |
|---|---|---|
| siteUrl* | string | the site with its scheme, e.g. "https://example.com" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"siteUrl": {
"type": "string",
"description": "the site with its scheme, e.g. \"https://example.com\""
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| level | string | where to attach it — follows the id you pass; customer = every campaign on the account |
| assets | array | the assets to CREATE and attach |
| dryRun | boolean | validate against Google and create NOTHING |
| status | string | the LINK status — default ENABLED |
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.
| Argument | Type | Description |
|---|---|---|
| dryRun | boolean | true = validate with Google and create nothing |
| 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 |
| keywords* | array | the keywords to add |
| adGroupId* | string | the ad group to add them to |
| customerId | string | omit to use the brand’s selected default account |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true to add an Active keyword to a LIVE ad group |
| keywords* | array | the keywords |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId* | string | the ad group to add them to |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"keywords"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true to add an Active keyword to a LIVE ad group"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"bid": {
"type": "number",
"description": "per-keyword max CPC in the account currency"
},
"text": {
"type": "string",
"description": "≤100 characters"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "default Paused"
},
"matchType": {
"enum": [
"Exact",
"Phrase",
"Broad"
],
"type": "string",
"description": "default Phrase — Microsoft has no broad-match-modifier"
}
}
},
"description": "the keywords"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "the ad group to add them to"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| keywords* | array | negative keywords, each with text and matchType (Exact or Phrase) |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId | string | pass this OR campaignId |
| campaignId | string | campaign to add negatives to; pass this or adGroupId |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"keywords"
],
"properties": {
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "≤100 characters"
},
"matchType": {
"enum": [
"Exact",
"Phrase"
],
"type": "string",
"description": "default Phrase — Microsoft has no broad negative"
}
}
},
"description": "negative keywords, each with text and matchType (Exact or Phrase)"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "pass this OR campaignId"
},
"campaignId": {
"type": "string",
"description": "campaign to add negatives to; pass this or adGroupId"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}Add a property to the connected Google account. TWO THINGS THAT MUST REACH THE USER, and the answer states both: (1) ADDING IS NOT VERIFYING — the property arrives with the account as an unverified user and EVERY read on it is refused until ownership is proven with a DNS record, an HTML file or a tag, which no API can do and which the user completes in Search Console itself; (2) the new property is NOT yet shared with this profile, so someone has to tick it under Settings > Connectors > Google Search Console > Manage accounts (or call list_connector_accounts with provider google_search_console, then set_connector_accounts) before any tool here can use it. The permission level is READ BACK from Google, so the answer says which of those two states it is actually in. 0 credits.
| Argument | Type | Description |
|---|---|---|
| siteUrl* | string | "sc-domain:example.com" for a Domain property (covers every scheme and subdomain), or the full URL-prefix form "https://example.com/". These are different properties — pick deliberately. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"siteUrl": {
"type": "string",
"description": "\"sc-domain:example.com\" for a Domain property (covers every scheme and subdomain), or the full URL-prefix form \"https://example.com/\". These are different properties — pick deliberately."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| criteria | array | [{targetingType, targetingValue, operatorType?}] — operatorType defaults to EQ |
| accountId | string | older name for adAccountId (MCP only) |
| lineItemId* | string | line item id to target, from list_x_ads_line_items |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| locationIds | array | opaque ids from x_ads_geo_search |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"lineItemId"
],
"properties": {
"criteria": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "[{targetingType, targetingValue, operatorType?}] — operatorType defaults to EQ"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"lineItemId": {
"type": "string",
"description": "line item id to target, from list_x_ads_line_items"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "opaque ids from x_ads_geo_search"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| end | string | YYYYMMDD or YYYY-MM-DD (default today) |
| start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| metric | string | default active |
| groupBy | string | ONE property to group by |
| interval | number | 1 daily, 7 weekly, 30 monthly |
| segments | array | 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. |
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.
| Argument | Type | Description |
|---|---|---|
| chartId* | string | the saved chart's id, or its full URL — the id is the segment after /chart/ |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"chartId"
],
"properties": {
"chartId": {
"type": "string",
"description": "the saved chart's id, or its full URL — the id is the segment after /chart/"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| end | string | YYYYMMDD or YYYY-MM-DD (default today) |
| mode | string | default ordered |
| start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| users | string | 'new' = first-time users only (the acquisition funnel); Amplitude's default is active |
| events* | array | ordered steps — plain event names, or {eventType, filters:[…]} objects |
| groupBy | string | at most ONE — a second is refused |
| segments |
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.
| Argument | Type | Description |
|---|---|---|
| end | string | YYYYMMDD or YYYY-MM-DD (default today) |
| start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| groupBy | string | ONE property to group by |
| brackets | array | required when retentionMode is "bracket" |
| interval | number | 1 daily, 7 weekly, 30 monthly |
| segments | array | 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 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.
| Argument | Type | Description |
|---|---|---|
| end | string | YYYYMMDD or YYYY-MM-DD (default today) |
| event | string | shorthand for a single event name |
| limit | number | ≤1000 |
| start | string | YYYYMMDD or YYYY-MM-DD (default 30 days ago) |
| events | array | one or two event specs: a plain name, or {eventType, filters:[{subprop_type,subprop_key,subprop_op,subprop_value}], groupBy:[…]} |
| metric | string | default uniques |
| formula |
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.
| Argument | Type | Description |
|---|---|---|
| user* | string | a numeric Amplitude ID to read activity, or a user id / device id / user-id prefix to search |
| limit | number | maximum events to return, up to 1000 |
| search | boolean | force a search even when the value is numeric |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"user"
],
"properties": {
"user": {
"type": "string",
"description": "a numeric Amplitude ID to read activity, or a user id / device id / user-id prefix to search"
},
"limit": {
"type": "number",
"description": "maximum events to return, up to 1000"
},
"search": {
"type": "boolean",
"description": "force a search even when the value is numeric"
}
}
}HOW LONG GA4 KEEPS EVENT- AND USER-LEVEL DATA. The default is 2 months, which silently caps every exploration and funnel at the last two months; standard reports are unaffected. action:'get' reads it; 'set' changes eventDataRetention / userDataRetention (2 or 14 months on a standard property; 26, 38, 50 for event data on Analytics 360 only) and resetUserDataOnNewActivity. RAISING applies to data already collected. LOWERING DELETES the older data at Google's next monthly run and cannot be undone, so it answers needsConfirm first — show the user, get a yes, then confirm:true. Read back. Free.
| Argument | Type | Description |
|---|---|---|
| action | string | defaults to 'get' |
| confirm | boolean | required when the change SHORTENS retention, which deletes data |
| property* | string | NUMERIC GA4 property id, from list_analytics_properties |
| userDataRetention | string | months: 2 or 14 |
| eventDataRetention | string | months: 2 or 14 (26/38/50 Analytics 360 only), or the enum e.g. FOURTEEN_MONTHS |
| resetUserDataOnNewActivity | boolean | true = a user's data retention window restarts each time that user shows new activity |
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.
| Argument | Type | Description |
|---|---|---|
| limit | number | rows, 1–1000 (default 50) |
| metrics | array | realtime metric names — default ["activeUsers"]. NOT the same set as analytics_report; `sessions` is not a realtime metric |
| property* | 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 |
| dimensions | array | realtime dimension names, e.g. ["country"] or ["unifiedScreenName"] |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"limit": {
"type": "number",
"description": "rows, 1–1000 (default 50)"
},
"metrics": {
"type": "array",
"items": {
"type": "string"
},
"description": "realtime metric names — default [\"activeUsers\"]. NOT the same set as analytics_report; `sessions` is not a realtime metric"
},
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name"
},
"dimensions": {
"type": "array",
"items": {
"type": "string"
},
"description": "realtime dimension names, e.g. [\"country\"] or [\"unifiedScreenName\"]"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| 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. |
| totals | boolean | add TOTAL rows |
Use this when the user asks how their paid ad campaigns are doing or what to scale or pause. ONE verdict across EVERY ad platform this workspace has connected. It pulls each platform's own report (Meta, Google Ads, ChatGPT Ads, TikTok, LinkedIn, Microsoft Advertising, Pinterest, Reddit, Snapchat, X, Apple Ads), normalises every campaign / ad set / ad into ONE table — spend, impressions, clicks, CTR, CPC, conversions, CPA, ROAS and frequency where the platform reports them — and answers with four lists: `scale`, `pause`, `fix` and `test_next`, plus `insufficient_data`. Each recommendation carries the row's OWN numbers as evidence, the reason, a concrete next action, and the EXACT Hermoso tool call that would apply it (for example set_meta_campaign_status with confirm) — which this tool never runs. FOUR REFUSALS TO REPEAT RATHER THAN PAPER OVER: (1) A ROW BELOW THE DATA FLOOR IS NEVER JUDGED — the minimums are stated in the reply, and an under-powered row lands in insufficient_data with what it lacks, never in "pause"; a row whose conversions are too few for a CPA verdict can still be read on CTR and CPC and says so. (2) A PLATFORM WHOSE READ FAILED IS NAMED AS UNREADABLE and its numbers are MISSING, never zero — never move budget on the strength of an absence. (3) A META AD SET IN THE LEARNING PHASE IS HELD BACK, because editing it restarts learning. (4) A CONVERSION COLUMN THE PLATFORM DOES NOT PUBLISH READS UNMEASURED, not zero (Reddit, Snapchat and X in this report). Conversion definitions differ per platform and travel with every row, and each platform's days are its own account's days, so a cross-platform CPA comparison must carry that caveat. Args: days (complete days ending yesterday, default 14, max 90), platforms (omit for every connected one), goal ("lowest CPA", "ROAS", "leads under $30"), level (campaign | ad_group | ad — a platform with no report at that tier is read at campaign level and says so). Changes nothing in any ad account and spends no ad money; it bills one model call over the table.
| Argument | Type | Description |
|---|---|---|
| days | number | how many complete days back, ending yesterday (default 14, max 90) |
| goal |
Append text to the end of a Google Doc Hermoso can reach — one it created (pass the documentId from create_doc) or one the user handed over with the Google file picker in the app (find its id with list_drive_files). Optional `dropdown:{title, options:[2-50], selected}` appends a Google Docs dropdown chip after the text — e.g. text "Status: " + dropdown {title:"Status", options:["Draft","In review","Approved"]} gives a brief a status the team can click to change; the chip is confirmed by reading the doc back. Set it later with update_doc dropdowns.
| Argument | Type | Description |
|---|---|---|
| text | string | text to append at the end of the doc (optional when a dropdown is passed) |
| dropdown | object | a dropdown chip to append after the text |
| documentId* | string | the document id from create_doc |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"documentId"
],
"properties": {
"text": {
"type": "string",
"description": "text to append at the end of the doc (optional when a dropdown is passed)"
},
"dropdown": {
"type": "object",
"required": [
"options"
],
"properties": {
"title": {
"type": "string",
"description": "the dropdown title, e.g. \"Status\""
},
"options": {
"type": "array",
"items": {
"type": "string"
},
"description": "2 to 50 distinct options"
},
"selected": {
"type": "string",
"description": "the option it starts on (default: the first)"
}
},
"description": "a dropdown chip to append after the text"
},
"documentId": {
"type": "string",
"description": "the document id from create_doc"
}
}
}Add to the END of an existing Notion page (pageId or its link, from list_notion_pages): markdown content, a swipefileCollection, or includeSchedule:true. Nothing already on the page changes. Free.
| Argument | Type | Description |
|---|---|---|
| pageId* | string | page id or link |
| content | string | markdown to add |
| includeSchedule | boolean | true appends the scheduled posts too |
| swipefileCollection | string | swipefile collection name or id to append, with each ad's creative, copy, dates and link |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pageId"
],
"properties": {
"pageId": {
"type": "string",
"description": "page id or link"
},
"content": {
"type": "string",
"description": "markdown to add"
},
"includeSchedule": {
"type": "boolean",
"description": "true appends the scheduled posts too"
},
"swipefileCollection": {
"type": "string",
"description": "swipefile collection name or id to append, with each ad's creative, copy, dates and link"
}
}
}Append rows to a Google Sheet Hermoso can reach — one it created (pass the spreadsheetId from create_sheet) or one the user handed over with the Google file picker in the app (find its id with list_drive_files). rows = array of row arrays.
| Argument | Type | Description |
|---|---|---|
| rows* | array | rows to append — array of row arrays |
| range | string | range to append at (default A1 / first sheet) |
| spreadsheetId* | string | the spreadsheet id from create_sheet |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"spreadsheetId",
"rows"
],
"properties": {
"rows": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "rows to append — array of row arrays"
},
"range": {
"type": "string",
"description": "range to append at (default A1 / first sheet)"
},
"spreadsheetId": {
"type": "string",
"description": "the spreadsheet id from create_sheet"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| end | string | YYYY-MM-DD. |
| limit | number | maximum number of changes to return |
| start | string | YYYY-MM-DD. Required unless end or an explicit eventTime filter is given. |
| offset | number | changes to skip, for paging |
| filters | array | Extra conditions, e.g. [{"field":"campaignId","operator":"EQUALS","value":"123"}]. Non-time fields accept only EQUALS and IN. |
| 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. |
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.
| Argument | Type | Description |
|---|---|---|
| end* | string | REQUIRED YYYY-MM-DD. |
| limit | number | maximum number of rows to return |
| start* | string | REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT. |
| offset | number | rows to skip, for paging |
| reportType | string | Default FIRST_SLOT. |
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.
| Argument | Type | Description |
|---|---|---|
| kind | string | Default keyword. Each kind takes DIFFERENT filters — see the description. |
| limit | number | maximum number of suggestions to return |
| terms | array | kind:"keyword" ONLY. Seed search terms to expand from, e.g. ["productivity","task management"]. Refused on phrase/category. |
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.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of reasons to return |
| adamId | string | the app’s App Store id — every reason Apple holds for it |
| offset | number | reasons to skip, for paging |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of reasons to return"
},
"adamId": {
"type": "string",
"description": "the app’s App Store id — every reason Apple holds for it"
},
"offset": {
"type": "number",
"description": "reasons to skip, for paging"
},
"mapsBrandId": {
"type": "string",
"description": "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": {
"type": "string",
"description": "one reason by id"
}
}
}Apple Ads performance — impressions, taps, installs, spend, TTR, CPT, CPA. level is campaign / adgroup / ad / keyword / searchterm; promotedObjectType is apps (App Store, the default) or business-brands (Ads on Apple Maps). startTime and endTime are REQUIRED, as YYYY-MM-DD. campaignId is REQUIRED for every level except campaign — it moved from the URL into a filter, but Apple still enforces it (measured: “campaignId filter is required for AD_GROUP reports when promotedObjectType is APPS”), so only a campaign-level report may be account-wide. adGroupId narrows it further. groupBy is restricted PER LEVEL and an unsupported dimension is refused by name, never dropped: campaign and adgroup take deviceClass/ageRange/gender/countryCode/adminArea/locality/storefront/countryOrRegion, keyword and searchterm take only deviceClass/storefront/countryOrRegion, and ad takes only storefront/countryOrRegion. granularity is optional and carries Apple’s own date rules (HOURLY reaches back 7 days and is unavailable on ad and searchterm reports; DAILY 90 days and needs a range longer than one day; WEEKLY 365 days with an end date at least 14 days ago; MONTHLY needs an end date at least 90 days ago). FOR A SINGLE DAY, OMIT granularity — the totals come back in each row’s totalMetrics. Search-term reports are ORTZ-only. grandTotals adds a summary row; emptyMetrics includes entities with no delivery — App Store reports only, never together with groupBy, and never on a search-term report. A report with NO rows genuinely means there was NO delivery in that window and scope: say exactly that, and never present zeros as measured performance. Read-only and free, so run it first after connecting — it proves the credentials work with zero spend risk.
| Argument | Type | Description |
|---|---|---|
| level | string | default campaign |
| limit | number | rows per page, up to 5000 (default 100) |
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.
| Argument | Type | Description |
|---|---|---|
| end* | string | REQUIRED YYYY-MM-DD. |
| genre | array | App Store genre names, free text, e.g. ["PRODUCTIVITY_UTILITIES"]. |
| limit | number | maximum number of rows to return |
| start* | string | REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT. |
| offset | number | rows to skip, for paging |
What Apple thinks an install should cost you. Returns the suggested target CPA for an app — the MAXIMUM tap-install CPI Apple observed across that app’s eligible markets over the last 28 days, together with the market that produced it. Only markets with at least 10 installs in that window qualify, so a brand-new or low-volume app legitimately has no suggestion yet and that is an answer, not a 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.
| Argument | Type | Description |
|---|---|---|
| promotedObjectId* | string | REQUIRED — the app’s adamId. |
| countriesOrRegions | array | ISO 3166-1 alpha-2 markets to consider, e.g. ["US","GB","CA"]. |
| promotedObjectType | string | Default APPSTORE_APP. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"promotedObjectId"
],
"properties": {
"promotedObjectId": {
"type": "string",
"description": "REQUIRED — the app’s adamId."
},
"countriesOrRegions": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO 3166-1 alpha-2 markets to consider, e.g. [\"US\",\"GB\",\"CA\"]."
},
"promotedObjectType": {
"enum": [
"APPSTORE_APP",
"BUSINESS_BRAND"
],
"type": "string",
"description": "Default APPSTORE_APP."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | from 1 |
| size | number | 1–100, default 100 |
| since | string | YYYY-MM-DD UTC, with until |
| until | string | YYYY-MM-DD UTC, with since |
| campaignId | string | ONE campaign id; omit for every campaign |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "from 1"
},
"size": {
"type": "number",
"description": "1–100, default 100"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD UTC, with until"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD UTC, with since"
},
"campaignId": {
"type": "string",
"description": "ONE campaign id; omit for every campaign"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| eventKey | string | the AppLovin Event Key; omit to use the one saved on the connection |
| platform | string | where it is installed, default html |
| siteType | string | default purchase |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"eventKey": {
"type": "string",
"description": "the AppLovin Event Key; omit to use the one saved on the connection"
},
"platform": {
"enum": [
"html",
"gtm",
"shopify"
],
"type": "string",
"description": "where it is installed, default html"
},
"siteType": {
"enum": [
"purchase",
"lead"
],
"type": "string",
"description": "default purchase"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | 1–10000, default 1000 |
| since | string | YYYY-MM-DD, default 7 days ago |
| until | string | YYYY-MM-DD, default today |
| cohort | boolean | attribute to impression day |
| 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) |
| offset | number | rows to skip, for paging past the limit |
| columns | array | e.g. day, campaign, campaign_id_external, creative_set, country, impressions, clicks, ctr, cost, sales, roas_7d, chka_usd_7d |
| filters | object | column -> values |
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.
| Argument | Type | Description |
|---|---|---|
| id* | string | REQUIRED — the recommendation id from list_apple_ads_recommendations. |
| type | string |
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.
| Argument | Type | Description |
|---|---|---|
| action | string | Add (default) is additive across calls |
| emails | array | 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 |
| subType | string | Email is the only identifier Microsoft supports here — it publishes Phone and CRM as “Not currently supported” |
| accountId | string |
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.
| Argument | Type | Description |
|---|---|---|
| dryRun | boolean | report what would happen and apply nothing at all |
| 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 |
| accountId | string |
Attach a TikTok custom audience to ad groups as an INCLUDE or an EXCLUDE — the call that changes WHO SEES THE ADS, and the reason building audiences is worth doing at all. actionMode is exactly "Apply" or "Disconnect" (TikTok’s enum is case-sensitive), and Apply also needs usageMode "Include" or "Exclude": GETTING usageMode WRONG INVERTS THE ENTIRE AUDIENCE, showing the ads to precisely the people you meant to keep out, and nothing in TikTok’s reply would say so. One audience per call, and the audience and ad groups must be on the same advertiser. A Lookalike cannot be applied to a Reach & Frequency ad group. It spends nothing by itself — every ad group Hermoso builds is born paused — but on an ALREADY-LIVE ad group it changes delivery on the next auction, so name the ad groups to the user first. The ad groups are read back from TikTok.
| Argument | Type | Description |
|---|---|---|
| usageMode | string | REQUIRED when actionMode is Apply, and refused with Disconnect |
| actionMode* | string | Apply attaches the audience to the ad groups; Disconnect removes it (case-sensitive) |
| adgroupIds* | array | from list_tiktok_ads_campaigns |
| audienceId* | string | exactly one — TikTok takes a single custom_audience_id per call |
| advertiserId | string | from list_tiktok_ads_accounts |
ARCHIVE a GA4 audience, and it is ONE-WAY: GA4 publishes no delete and no un-archive anywhere in its API. Any Google Ads remarketing list sourced from it stops refreshing, and because the definition is IMMUTABLE the audience cannot be recovered by editing — rebuilding means a NEW audience that starts collecting members from scratch, so months of accumulated membership are gone. WITHOUT `confirm` IT ARCHIVES NOTHING and describes the audience read back from Google; check that against what the user asked for, because naming the right audience is the one thing confirm cannot prove. An ambiguous displayName is REFUSED rather than first-matched — GA4 does not enforce unique names and there is no way back. 0 credits.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually archive |
| property* | string | the NUMERIC GA4 property id |
| audienceId | string | from list_analytics_audiences — preferred, because it is unambiguous |
| displayName | string | the audience name, if the id is not to hand. Refused when it matches more than one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually archive"
},
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id"
},
"audienceId": {
"type": "string",
"description": "from list_analytics_audiences — preferred, because it is unambiguous"
},
"displayName": {
"type": "string",
"description": "the audience name, if the id is not to hand. Refused when it matches more than one"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually archive. Without it nothing changes and the dimension is described back to you |
| property* | string | the NUMERIC GA4 property id from list_analytics_properties |
| parameterName* | string | the event parameter of the dimension to archive, e.g. "customer_tier" — from list_analytics_definitions, NOT the report label |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"parameterName"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually archive. Without it nothing changes and the dimension is described back to you"
},
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties"
},
"parameterName": {
"type": "string",
"description": "the event parameter of the dimension to archive, e.g. \"customer_tier\" — from list_analytics_definitions, NOT the report label"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually archive — without it nothing is archived and the metric is described back to you |
| property* | string | NUMERIC GA4 property id |
| parameterName* | string | the event parameter of the metric to archive, e.g. 'order_value' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"parameterName"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually archive — without it nothing is archived and the metric is described back to you"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id"
},
"parameterName": {
"type": "string",
"description": "the event parameter of the metric to archive, e.g. 'order_value'"
}
}
}Retire an INSTANT LEAD FORM so it stops collecting — the only way to tidy a Page’s Instant Forms list, because META DOES NOT SUPPORT DELETING A LEAD FORM AT ALL, only archiving it. This is REVERSIBLE: pass status:"ACTIVE" to switch an archived form back on. Archiving publishes nothing, spends nothing and DELETES NO LEADS — everything the form already collected stays readable with read_meta_leads. CALL IT WITHOUT confirm FIRST: nothing changes and you get the form’s real name, current status and lead count READ BACK FROM META, so you can show the user exactly which form they are about to retire rather than echoing back an id they may have mistyped; then call again with confirm:true. WARNING WORTH RELAYING BEFORE ARCHIVING: if a live ad still points at this form, its button will open nothing — check the ad first. The new status is READ BACK from Meta after the change, so the summary reports what Meta actually stored and says plainly when the change did NOT take; print it verbatim rather than assuming a successful call means a changed form.
| Argument | Type | Description |
|---|---|---|
| formId* | string | the form to retire (from list_meta_lead_forms) |
| pageId | string | which connected Page (required only if the profile has more than one) |
| status | string | default ARCHIVED; "ACTIVE" restores a form that was archived |
| confirm | boolean | must be true to actually change it — without it nothing changes and you get the read-back to show the user |
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually archive — this cannot be undone |
| audienceId* | string | ChatGPT Ads audience id to archive; archiving is permanent |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually archive — this cannot be undone"
},
"audienceId": {
"type": "string",
"description": "ChatGPT Ads audience id to archive; archiving is permanent"
}
}
}Archive a product feed — its products leave every campaign using it. confirm:true required.
| Argument | Type | Description |
|---|---|---|
| feedId* | string | product feed id to archive |
| confirm | boolean | true to archive; its products leave every campaign that uses it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"feedId"
],
"properties": {
"feedId": {
"type": "string",
"description": "product feed id to archive"
},
"confirm": {
"type": "boolean",
"description": "true to archive; its products leave every campaign that uses it"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to archive; archiving cannot be reversed through the API |
| leadFormId* | string | lead form id to archive |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"leadFormId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to archive; archiving cannot be reversed through the API"
},
"leadFormId": {
"type": "string",
"description": "lead form id to archive"
}
}
}Associate LinkedIn campaigns with a conversion rule — or detach them with remove:true. THIS IS WHAT MAKES A CONVERSION COUNT: LinkedIn only attributes a conversion to campaigns associated with its rule, so an unassociated rule reports zero however many events you stream to it. Associate every campaign the conversion could plausibly have come from. It spends nothing, but on a LIVE campaign it changes what the campaign optimises toward and what its report counts. The result is the PER-CAMPAIGN status LinkedIn returned, not a blanket success — repeat any that failed. Free.
| Argument | Type | Description |
|---|---|---|
| remove | boolean | detach instead of attach |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
| campaignIds* | array | LinkedIn campaign ids (list_linkedin_ads_campaigns has them) |
| conversionId* | string | the conversion rule id, or its urn:lla:llaPartnerConversion:… URN |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"conversionId",
"campaignIds"
],
"properties": {
"remove": {
"type": "boolean",
"description": "detach instead of attach"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "LinkedIn campaign ids (list_linkedin_ads_campaigns has them)"
},
"conversionId": {
"type": "string",
"description": "the conversion rule id, or its urn:lla:llaPartnerConversion:… URN"
}
}
}Apply an authorization code so this ad account may promote somebody else’s TikTok post. THE CODE CANNOT BE MINTED FROM THE API — only the post’s owner can generate one, in the TikTok app: open the post -> three-dot menu -> Ad settings -> toggle Ad authorization -> choose a duration -> copy the code. Ask them for it; there is no way around this and no endpoint that creates one. Applying a code SPENDS NOTHING and creates nothing that can serve — it only grants permission — so it is deliberately not confirm-gated. If the post is a DUET or STITCH of another post, or mentions one, TikTok requires the ORIGINAL post owner’s code too: pass it as originalPostAuthCode or the call is refused. THE ANSWER IS THE READ-BACK: the reply lists the posts now authorised, because TikTok’s own response body is empty and a bare 200 proves nothing.
| Argument | Type | Description |
|---|---|---|
| authCode* | string | the code the post owner generated in the TikTok app. Paste it verbatim — Hermoso handles the + / %2B escaping TikTok warns about. |
| advertiserId | string | TikTok advertiser id from list_tiktok_ads_accounts; omit when only one is connected |
| originalPostAuthCode | string | REQUIRED when the post is a duet/stitch of, or mentions, another post — the ORIGINAL post owner’s code |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"authCode"
],
"properties": {
"authCode": {
"type": "string",
"description": "the code the post owner generated in the TikTok app. Paste it verbatim — Hermoso handles the + / %2B escaping TikTok warns about."
},
"advertiserId": {
"type": "string",
"description": "TikTok advertiser id from list_tiktok_ads_accounts; omit when only one is connected"
},
"originalPostAuthCode": {
"type": "string",
"description": "REQUIRED when the post is a duet/stitch of, or mentions, another post — the ORIGINAL post owner’s code"
}
}
}Show this account's billing at a glance: current plan (quote plan.priceUsd per plan.period), credit balance, whether auto-reload is on, whether a card is on file, and whether YOU (this key) have ADMIN rights to change billing. Read-only, free. Call it before upgrade_plan / set_auto_reload to know what's possible — members have read-only billing. IN A SHARED TEAM WORKSPACE A MEMBER SEES THE PLAN AND THE BALANCE ONLY: the workspace owner's payment card and auto-reload belong to them and are not reported (billingScope:'member'). Never tell a member there is no card on file — the honest answer is that you cannot see it. plan.memberRate: media models at cost + 10%; plan.legacy: an older plan kept at its price. posting: this month's posting allowance ("N of 10 posts used this month" on Free, unlimited on every paid plan).
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Bind campaigns, ad groups or ads to an EXISTING automated rule, or unbind them from one. BINDING TO A RULE THAT IS RUNNING AND ABLE TO SPEND puts those objects under standing permission to have their budget or bid raised, which is the same act as arming the rule for them — so it takes the same confirm:true plus confirmScope echo, computed from the rule as TikTok stores it. UNBINDING only ever narrows a rule’s reach and needs neither. TikTok answers a bind with an empty body, so the result is read back off the rule rather than taken from the 200. Free.
| Argument | Type | Description |
|---|---|---|
| ruleId* | string | from list_tiktok_ads_rules |
| confirm | boolean | required to BIND onto a live rule that can spend |
| bindType* | string | BIND puts the objects under the rule; UNBIND takes them out |
| dimension* | string | level of the objects in dimensionIds: CAMPAIGN, ADGROUP or AD |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| confirmScope | string | required in the same case — the scopeToken from list_tiktok_ads_rules |
| dimensionIds* | array |
Whether Bing can actually reach the site's pages: crawl statistics (mode "stats"), the URLs with problems (mode "issues", the default), one page's index detail (mode "page", needs `url`), a directory's (mode "directory"), or the crawl settings themselves (mode "settings"). Issue names are DECODED from Bing's bitmask into readable ones — Code301, Code302, Code4xx, Code5xx, BlockedByRobotsTxt, ContainsMalware, ImportantUrlBlockedByRobotsTxt, DnsErrors, TimeOutErrors — because the raw JSON hands back an integer that means nothing on its own, and a bit Microsoft has added since is reported as unknown rather than dropped. Microsoft notes it may take a few days for a FIXED issue to disappear from this list, so a listed issue is not proof it is still live. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| url | string | required for page and directory modes |
| mode | string | issues (default) | stats | page | directory | settings |
| page | number | zero-based results page, directory mode |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"url": {
"type": "string",
"description": "required for page and directory modes"
},
"mode": {
"type": "string",
"description": "issues (default) | stats | page | directory | settings"
},
"page": {
"type": "number",
"description": "zero-based results page, directory mode"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}REAL SEARCH DEMAND FOR ANY TERM, free. Modes: "keyword" (impressions for exactly this term over a date range), "related" (the same for terms Bing considers related — the one to use for expanding a seed into a list), "history" (the term's historical series). This is the closest thing in Hermoso to a free keyword planner: google_ads_keyword_ideas needs Google Ads Basic access, and this needs only the Bing connection anyone can set up in two minutes. NOT SITE-SCOPED — these are Microsoft's signatures and they take no site, so it works for ANY term including a competitor's brand, and no property tick list applies. `country` AND `language` are BOTH REQUIRED and are deliberately never defaulted: a search volume with no market is unactionable, and picking one for you would answer confidently about the wrong country. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| q* | string | the keyword to look up, e.g. "running shoes" |
| mode | string | keyword (default) | related | history |
| country* | string | REQUIRED, e.g. "us" — there is no default and one will not be guessed |
| endDate | string | YYYY-MM-DD — required for keyword and related modes |
| language* | string | REQUIRED, e.g. "en-US" |
| startDate |
Inbound links according to Bing: counts per page (mode "counts", the default), the inbound links pointing at one page (mode "page", needs `url`), or the pages the site owner has declared as connected (mode "connected"). Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| url | string | required for page mode — the page whose inbound links you want |
| mode | string | counts (default) | page | connected |
| page | number | zero-based results page |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"url": {
"type": "string",
"description": "required for page mode — the page whose inbound links you want"
},
"mode": {
"type": "string",
"description": "counts (default) | page | connected"
},
"page": {
"type": "number",
"description": "zero-based results page"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}The Bing twin of search_console_performance, split by what you want pinned. Modes: "queries" (top search terms for the site, the default), "pages" (top pages), "pageQueries" (which queries brought traffic to one `page`), "queryPages" (which pages one `query` landed on), "queryPageDetail" (one query on one page, day by day). MICROSOFT UPDATES THIS DATA WEEKLY by their own statement, so it lags Search Console and will not move day to day; do not report a flat week as a drop. An unknown mode is refused by name. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| mode | string | queries (default) | pages | pageQueries | queryPages | queryPageDetail |
| page | string | the full page URL — required for pageQueries and queryPageDetail |
| query | string | required for queryPages and queryPageDetail |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"mode": {
"type": "string",
"description": "queries (default) | pages | pageQueries | queryPages | queryPageDetail"
},
"page": {
"type": "string",
"description": "the full page URL — required for pageQueries and queryPageDetail"
},
"query": {
"type": "string",
"description": "required for queryPages and queryPageDetail"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}How many URLs this site may still submit to Bing today and this month, plus the separate content-submission budget. BING SETS THIS PER SITE AND IT VARIES — Microsoft's own documented example allows 5 a day and 24 a month — so it is always read live and never assumed. Check it before promising anyone a bulk submission, because Bing rejects an over-quota batch as a WHOLE rather than taking what fits. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}Bing clicks and impressions over time — for the whole site (mode "site", the default), for one page (mode "page", needs `url`), or for every page under one directory (mode "directory", needs `url`). An unknown mode is refused by name. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| url | string | required for page and directory modes — the full URL |
| mode | string | site (default) | page | directory |
| page | number | zero-based results page, directory mode |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites, and it must be one SHARED with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"url": {
"type": "string",
"description": "required for page and directory modes — the full URL"
},
"mode": {
"type": "string",
"description": "site (default) | page | directory"
},
"page": {
"type": "number",
"description": "zero-based results page, directory mode"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites, and it must be one SHARED with this profile"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| op | string | Default create. |
| kind | string | Default keyword. |
| items* | array | REQUIRED. create: [{adGroupId,text,matchType,bid?}] for keywords, or [{campaignId|adGroupId,text,matchType}] for negatives. update: [{id,bid?,status?}]. |
| confirm | boolean | Required only when the target ad group or campaign is already ENABLED, where the new keywords could serve immediately. |
Out of credits? Top up with a credit PACK. Call with no argument to list the available packs (id · credits · price). If the account has a saved card and you have billing-admin rights, calling with `pack` quotes the exact charge and calling again with confirm:true AND the quote's quote_token charges the saved card instantly (same one-click top-up as the app — no redirect). If there's no saved card yet, you get a Stripe checkout URL to hand your human for the FIRST purchase; their card saves for one-click after that. Packs only; subscriptions are managed by a person in Billing. IF YOU ARE AN AGENT HOLDING YOUR OWN PAYMENT CREDENTIAL, there is a third path that needs no human at all: `POST /api/billing/machine-payment` with a `packId` answers HTTP 402 carrying an MPP challenge, and grants the pack once you authorise and retry with the credential — the same packs, the same prices, the same credits. `GET /api/billing/config` carries a `machinePayments` block listing the packs with their per-credit rates and saying whether that lane is enabled on this server. Most agents do NOT have their own credential yet, so the checkout link above remains the normal path. To stop running out entirely, turn on auto-reload with set_auto_reload (admin) — low balances then top themselves up from the saved card automatically.
| Argument | Type | Description |
|---|---|---|
| pack | string | the pack id to buy (e.g. pack-2k) — omit to list the available packs first |
| confirm | boolean | set true to actually charge the saved card for `pack` (required for the one-click charge; ignored on the checkout-link path) |
| quote_token | string | the quoteToken returned by the quote step — REQUIRED (with confirm:true) to charge; it binds the exact pack + price you quoted (10-minute validity) and makes a retried confirm idempotent |
Run ANY Hermoso tool by name — including the paid-campaign, analytics and channel-admin tools that are not in this session's starting list — with the same permissions, the same account and the same result as calling it directly. Get the exact `name` and its `args` from find_tools first. This is the route on hosts that cannot reload their tool list mid-conversation (claude.ai, ChatGPT): enable_tools switches a group on server-side, but such a host keeps the list it fetched at connect time. Arguments are validated against the tool's own schema and a mistake is answered with the expected parameters, not a silent default. Refused by name, with the way out, when the tool needs a connector this workspace has not made or is withheld by the host's own policy.
| Argument | Type | Description |
|---|---|---|
| args | object | the tool's arguments as an object, exactly as its own schema takes them |
| name* | string | the tool name exactly as find_tools returned it, e.g. create_meta_lead_form |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"args": {
"type": "object",
"description": "the tool's arguments as an object, exactly as its own schema takes them",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"name": {
"type": "string",
"description": "the tool name exactly as find_tools returned it, e.g. create_meta_lead_form"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| audienceId* | string | audience the operation belongs to |
| operationId* | string | operation id returned by update_openai_ads_audience_members |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId",
"operationId"
],
"properties": {
"audienceId": {
"type": "string",
"description": "audience the operation belongs to"
},
"operationId": {
"type": "string",
"description": "operation id returned by update_openai_ads_audience_members"
}
}
}Cancel a Stripe subscription: at the end of the current period by default (the customer keeps access until then), or immediately:true to end it now. ALWAYS needs confirm:true, test keys included. The read-back distinguishes the two — a period-end cancel reports the date it will end and the status it keeps until then, an immediate one reports status canceled. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true after the user approves this cancellation |
| immediately | boolean | true = end now; omitted or false = end at the period end |
| subscriptionId* | string | subscription id, sub_... |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"subscriptionId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true after the user approves this cancellation"
},
"immediately": {
"type": "boolean",
"description": "true = end now; omitted or false = end at the period end"
},
"subscriptionId": {
"type": "string",
"description": "subscription id, sub_..."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| metrics | array | the same metrics you would send to analytics_report (defaults to the same three it defaults to) |
| property* | string | NUMERIC GA4 property id |
| dimensions | array | the same dimensions you would send to analytics_report |
| metricFilter | object | the same GA4 FilterExpression you would send to analytics_report |
| dimensionFilter | object | the same GA4 FilterExpression you would send to analytics_report |
Ask Apple whether an app may be advertised at all, PER PLACEMENT AND PER COUNTRY — the check Apple tells you to run before building a campaign around an app. Each row is ELIGIBLE or INELIGIBLE for one combination of placement, country or region and device class, with the minimum age rating for that market. A campaign targeting a country the app is INELIGIBLE in simply will not deliver there and nothing in the campaign build says so, so this is how you find out for free beforehand. adamId is REQUIRED. NO ROWS IS NOT THE SAME AS INELIGIBLE — it usually means the app is not owned by this organization; report that distinction rather than presenting it as a refusal. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of eligibility rows to return |
| adamId* | string | REQUIRED — from search_apple_ads_apps. |
| deviceClass | array | IPHONE and/or IPAD. |
| countryOrRegion | array | ISO 3166-1 alpha-2 codes. |
| supplyPlacement | array | Narrow to placements, e.g. ["APPSTORE_SEARCH_RESULTS","APPSTORE_SEARCH_TAB"]. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adamId"
],
"properties": {
"limit": {
"type": "number",
"description": "maximum number of eligibility rows to return"
},
"adamId": {
"type": "string",
"description": "REQUIRED — from search_apple_ads_apps."
},
"deviceClass": {
"type": "array",
"items": {
"type": "string"
},
"description": "IPHONE and/or IPAD."
},
"countryOrRegion": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO 3166-1 alpha-2 codes."
},
"supplyPlacement": {
"type": "array",
"items": {
"type": "string"
},
"description": "Narrow to placements, e.g. [\"APPSTORE_SEARCH_RESULTS\",\"APPSTORE_SEARCH_TAB\"]."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| handles* | array | TikTok usernames WITHOUT the @, max 20 |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"handles"
],
"properties": {
"handles": {
"type": "array",
"items": {
"type": "string"
},
"description": "TikTok usernames WITHOUT the @, max 20"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
}
}
}Empty a range of cells in a Google Sheet, leaving the rows themselves in place. DESTRUCTIVE: call it WITHOUT confirm first and nothing is cleared — you get back the real number of filled cells in that exact range. Show the user that number, get an unambiguous yes, then call again with confirm:true AND confirmCells set to it. The echo is not ceremony: it is what catches naming A1:Z1000 when you meant A1:Z10, which is the mistake that actually happens. There is deliberately NO default range. The clear is read back and reported as confirmed only if the range really is empty afterwards. To remove a whole tab instead, use manage_sheet_tabs.
| Argument | Type | Description |
|---|---|---|
| range* | string | the range to clear, e.g. "A2:D50" or "Sheet1!A2:D50" |
| confirm | boolean | true clears the range; call without it first to get the filled-cell count |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| confirmCells | number | echo back the filled-cell count the unconfirmed call reported |
| spreadsheetId | string | Google spreadsheet id; use this or sheetUrl |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"range"
],
"properties": {
"range": {
"type": "string",
"description": "the range to clear, e.g. \"A2:D50\" or \"Sheet1!A2:D50\""
},
"confirm": {
"type": "boolean",
"description": "true clears the range; call without it first to get the filled-cell count"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"confirmCells": {
"type": "number",
"description": "echo back the filled-cell count the unconfirmed call reported"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| campaignId* | string | the campaign to copy it into |
| creativeSetId* | string | creative set to copy; it must already belong to a campaign |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"creativeSetId",
"campaignId"
],
"properties": {
"campaignId": {
"type": "string",
"description": "the campaign to copy it into"
},
"creativeSetId": {
"type": "string",
"description": "creative set to copy; it must already belong to a campaign"
}
}
}Connect an account that links with a KEY rather than a sign-in screen, from here, with no browser: AppLovin Ads, Stripe, Intercom, Mailchimp, Klaviyo, Vercel, ChatGPT Ads, Apple Ads, Bluesky, Telegram, Bing Webmaster Tools, PostHog, Mixpanel, Amplitude, WordPress, Slack, Discord, Webhook. Pass provider and fields in that provider's own field names (listed below, * = required). The key is checked live with the provider before anything is saved, exactly as the app's Connectors page checks it, and the reply is read back from the saved connection. OFFER BOTH WAYS AND LET THE USER CHOOSE: a key pasted into this chat stays in this conversation's history, while pasting it in the app (Workspace > Connectors, or the one-click link https://app.hermoso.ai/?connect=<provider>) keeps it out of the chat. Hermoso never repeats a submitted key back. An account that connects through the provider's own sign-in screen (OAuth) cannot be connected here: this answers with the link to hand the user instead. Apple Ads with no key material first generates a signing key pair and returns the public key to register with Apple plus a setupToken to send back. Fields: applovin_ads {reportKey, eventKey, apiKey, capiKey, accountId} · stripe {apiKey*} · intercom {accessToken*, region} · mailchimp {apiKey*, server} · klaviyo {apiKey*} · vercel {token*, team} · openai_ads {apiKey*} · apple_ads {clientId, teamId, keyId, privateKey, setupToken, orgId} · bluesky {identifier*, appPassword*, pds} · telegram {token*} · bing_webmaster {apiKey*} · posthog {apiKey*, region, host, projectId} · mixpanel {username*, secret*, projectId*, region, workspaceId} · amplitude {apiKey*, secretKey*, region, host} · wordpress {siteUrl*, username*, appPassword*} · slack {webhookUrl, announceWebhookUrl} · discord {webhookUrl, announceWebhookUrl} · webhook {webhookUrl*}.
| Argument | Type | Description |
|---|---|---|
| fields | object | that provider's own field names and values, e.g. {"apiKey":"…"}; the names for each provider are in the description |
| provider |
Turn a file already in the user’s OneDrive into a PDF or a JPG — Microsoft does the conversion on its own servers, so nothing is re-encoded here and nothing is lost in a screenshot. It reads about 130 source formats, which is the point: PowerPoint and Word decks, Excel, Photoshop PSD, Illustrator AI, Sketch, 3D (fbx/glb/obj), video (mp4/mov/webm), HEIC from an iPhone, and the raw camera formats (CR2, NEF, ARW, DNG) that nothing else in this product can open. Use it to turn a client’s deck into images you can actually put in an ad, to get a usable JPG out of a designer’s PSD or a photographer’s raw file, or to hand someone a PDF of a spreadsheet. CONVERTING TO JPG REQUIRES BOTH width AND height — Microsoft refuses the call without them. The result is stored at a durable Hermoso URL you can pass straight to a render or a post; Microsoft’s own conversion link expires within minutes, so do not hand that one to anyone. Needs OneDrive connected — no new permission.
| Argument | Type | Description |
|---|---|---|
| width | number | REQUIRED for jpg — output width in pixels |
| fileId* | string | the OneDrive item id, from list_onedrive_files |
| format | string | default pdf |
| height | number | REQUIRED for jpg — output height in pixels |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"width": {
"type": "number",
"description": "REQUIRED for jpg — output width in pixels"
},
"fileId": {
"type": "string",
"description": "the OneDrive item id, from list_onedrive_files"
},
"format": {
"enum": [
"pdf",
"jpg"
],
"type": "string",
"description": "default pdf"
},
"height": {
"type": "number",
"description": "REQUIRED for jpg — output height in pixels"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| end | string | ISO 8601 — only for an annotation that spans a period |
| label* | string | the title shown on the chart, e.g. "Meta: summer UGC campaign live" |
| start* | string | ISO 8601 WITH time and offset, e.g. '2026-08-18T14:30:00+00:00' — a bare YYYYMMDD is refused |
| chartId | string | omit for a GLOBAL annotation that appears on every chart in the project |
BUILD A GA4 AUDIENCE — a remarketing list defined by what people did on the site, which flows into Google Ads across the property's Google Ads link. TELL THE USER TWO THINGS BEFORE CALLING: GA4 marks the DEFINITION, the MEMBERSHIP DURATION and every filter SCOPE immutable, so a wrong audience is archived and rebuilt rather than edited; and it is NOT retroactive — it starts collecting members from creation onward, and Google takes 24-48 hours to populate it. `filterClauses` is GA4's own nested shape and the TOP level of each filterExpression MUST be a single `andGroup` (Google's schema says so and its rejection names no field). Worked example, 'everyone who fired purchase': [{"clauseType":"INCLUDE","simpleFilter":{"scope":"AUDIENCE_FILTER_SCOPE_ACROSS_ALL_SESSIONS","filterExpression":{"andGroup":{"filterExpressions":[{"orGroup":{"filterExpressions":[{"eventFilter":{"eventName":"purchase"}}]}}]}}}}]. The result says whether the property is linked to Google Ads, because an audience nobody can remarket to is not a finished job. 0 credits.
| Argument | Type | Description |
|---|---|---|
| property* | string | the NUMERIC GA4 property id |
| description* | string | REQUIRED by Google on the Audience resource — one line saying who is in it |
| displayName* | string | the audience name shown in GA4 and in Google Ads |
| eventTrigger | object | optional {eventName, logCondition} event GA4 logs when a user joins the audience |
| filterClauses* | array |
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.
| Argument | Type | Description |
|---|---|---|
| scope | string | default EVENT. The 50-dimension cap applies to EVENT scope |
| property* | 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 |
| description | string | optional description, ≤150 characters |
| displayName | string | the label shown in GA4 reports — defaults to parameterName |
| parameterName* | string |
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.
| Argument | Type | Description |
|---|---|---|
| scope | string | EVENT is the only scope GA4 accepts for a metric, and it is immutable |
| property* | string | NUMERIC GA4 property id from list_analytics_properties — never the G-XXXXXXX Measurement ID |
| description | string | ≤150 characters |
| displayName | string | the label shown in reports; defaults to parameterName, max 82 characters |
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.
| Argument | Type | Description |
|---|---|---|
| property* | 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 |
| eventName* | string | an event GA4 already collects, e.g. "purchase" / "generate_lead" / "sign_up" |
| countingMethod | string | default ONCE_PER_EVENT; ONCE_PER_SESSION counts at most once per session (GA4 publishes no per-user method) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"eventName"
],
"properties": {
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name"
},
"eventName": {
"type": "string",
"description": "an event GA4 already collects, e.g. \"purchase\" / \"generate_lead\" / \"sign_up\""
},
"countingMethod": {
"enum": [
"ONCE_PER_EVENT",
"ONCE_PER_SESSION"
],
"type": "string",
"description": "default ONCE_PER_EVENT; ONCE_PER_SESSION counts at most once per session (GA4 publishes no per-user method)"
}
}
}Create an Apple Ads ad — the object that shows one creative in one ad group. REQUIRED: adGroupId, creativeId (from create_apple_ads_creative) and name. CREATED PAUSED ALWAYS, with no override, so it cannot spend until set_apple_ads_status(level:"ad", status:"ENABLED", confirm:true) and its parents are enabled too. THE PLACEMENT RULE, which Apple documents nowhere and which is checked FOR FREE before anything is written: an ad attaches only to a campaign whose placement is APPSTORE_SEARCH_TAB. Apple refuses ads on APPSTORE_SEARCH_RESULTS, APPSTORE_TODAY_TAB and APPSTORE_PRODUCT_PAGES campaigns with AD_CAMPAIGN_SUPPLY_SOURCES_NOT_SUPPORTED — and a SEARCH RESULTS campaign needs no ad and no creative at all, because Apple renders your App Store product page itself, so such a campaign is already complete. The creative must be VALID; a PENDING one is a wait, not a failure. Only ONE ad per ad group can be ENABLED at a time, so enabling this one means pausing whichever ad serves there now. Its creative and ad group are locked at creation: to serve a different creative, create a NEW ad and delete this one, which is Apple’s own instruction so delivery history stays traceable. NO CONFIRMATION IS NEEDED even inside a live ad group, and that is deliberate rather than an oversight: a paused ad cannot serve and does not displace whichever ad is serving there, so gating it would be friction with no safety behind it. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | REQUIRED. |
| adGroupId* | string | REQUIRED. |
| creativeId* | string | REQUIRED — from create_apple_ads_creative. |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | REQUIRED. |
| delegations | array | [{"resourceType":"CONTENT_PROVIDER","resourceId":"<CPID>"}] for App Store, or BUSINESS_BRAND + Brand ID for Maps. Must match productFeatures. |
| productFeatures* | string | REQUIRED and IMMUTABLE. App Store or Apple Maps, never both. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"productFeatures"
],
"properties": {
"name": {
"type": "string",
"description": "REQUIRED."
},
"delegations": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "[{\"resourceType\":\"CONTENT_PROVIDER\",\"resourceId\":\"<CPID>\"}] for App Store, or BUSINESS_BRAND + Brand ID for Maps. Must match productFeatures."
},
"productFeatures": {
"enum": [
"APPSTORE_APP_MANUAL",
"BUSINESS_BRAND_MANUAL"
],
"type": "string",
"description": "REQUIRED and IMMUTABLE. App Store or Apple Maps, never both."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | REQUIRED. |
| endTime | string | Omit to inherit the campaign end date. |
| currency | string | ISO 4217 currency code of the ad group bids |
| startTime | string | Defaults to now. |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | REQUIRED. |
| amount* | string | REQUIRED total budget as a decimal string, e.g. "50000.00". |
| endTime | string | Omit for an open-ended budget. |
| currency | string | ISO 4217; defaults to the ad account’s currency, which it must match. |
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.
| Argument | Type | Description |
|---|---|---|
| bid | string | MAX_CONVERSIONS only — the target CPA. Refused on MANUAL_CPT. |
| name* | string | REQUIRED. Unique across the entire Apple Ads organization. |
| endTime | string | Omit to run indefinitely. |
| currency | string | Defaults to the connected ad account’s currency; an account cannot mix currencies. |
| appAdamId |
Create an Apple Ads creative — what a person sees before and after tapping the ad. Pick creativeType: DEFAULT_PRODUCT_PAGE renders the app’s standard App Store listing and needs only adamId; CUSTOM_PRODUCT_PAGE renders a custom page built in App Store Connect and needs adamId AND productPageId (from list_apple_ads_product_pages); LOCAL_ADS_SEARCH_CREATIVE is Apple Maps and needs mapsBrandId (from list_apple_ads_brands) plus assetIds you uploaded with upload_apple_ads_asset — that is the one creative type where YOUR OWN IMAGE is the ad. YOU CANNOT SUPPLY YOUR OWN IMAGERY FOR AN APP STORE AD — Apple renders the pre-tap ad from the product page itself, so the way to change how an App Store ad LOOKS is to edit that page (or build a Custom Product Page) in App Store Connect; say that plainly rather than looking for a parameter that does not exist. The destination type is derived from the creative type and never asked for. A creative SPENDS NOTHING and has no status of its own: it serves only once an ad references it and that ad, its ad group and its campaign are all ENABLED. Apple validates asynchronously, so a fresh creative is usually PENDING for a moment and an ad may only reference a VALID one — that is a wait, not a failure. Type and destination are immutable afterwards. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | REQUIRED. |
| adamId | string | REQUIRED for App Store creatives — the App Store id. |
| brandId | string | older name for mapsBrandId (MCP only) |
| assetIds | array | Apple Maps only — ids from upload_apple_ads_asset. |
Create a location group — the set of a brand’s shops an Apple Maps ad group promotes. TWO KINDS. STATIC takes an explicit locationIds list (from list_apple_ads_locations) and is VALID immediately. DYNAMIC takes rules and maintains itself as the brand opens and closes shops, but starts PENDING while Apple evaluates them — wait for systemStatus VALID before targeting it. A RULE THAT MATCHES NOTHING IS APPLE’S ONE SILENT FAILURE HERE: it returns no error and simply builds a group with groupTotal 0, so adminArea must be the FULL English name ("California", never "CA") and locality must be countryOrRegion|adminArea|locality ("US|New York|Brooklyn", never "Brooklyn") — both shapes are refused here before the group is built. A group SPENDS NOTHING and does not decide who sees the ad or how near they are: it only scopes WHICH shops the ad group promotes, while radius and geo targeting on the ad group decide reach. mapsBrandId and the ad account are locked in permanently at creation. Target it afterwards with create_apple_ads_ad_group(targeting.locationGroupIds) or update_apple_ads_object(level:"adgroup"). Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | REQUIRED. |
| rules | array | REQUIRED for DYNAMIC. value is a string for EQUALS/NOT_EQUALS and an array for IN/NOT_IN. |
| brandId | string | older name for mapsBrandId (MCP only) |
| groupType | string | Default STATIC. Immutable afterwards. |
| description | string | description of the location group |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name |
| type | string | default: the connected account’s type |
| endDate | string | end date, YYYY-MM-DD |
| clickUrl | string | click-tracking URL from the measurement partner, for app campaigns |
| goalType* | string | WEB: CPE, CPP, CHK_ROAS |
| itunesId | number | iOS App Store numeric id, for iOS app campaigns |
| platform | string | app platform: IOS or ANDROID |
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.
| Argument | Type | Description |
|---|---|---|
| url | string | landing page for this set only (WEB) |
| name* | string | creative set name |
| assetIds* | array | AppLovin asset ids |
| brandInfo | object | override the account brand on THIS creative set — how a brand A/B tests the end card |
| countries | array | ISO country codes for the creative set |
| languages | array | e.g. ENGLISH |
| campaignId | string | campaign to place the creative set in; optional |
| useAccountBrandInfo | boolean |
Add a NEW profile (a brand, client, creator or personal workspace) and switch to it. Each has its OWN brand details, memory, swipefile, Library, avatars, skills, playbooks and connectors; nothing leaks between them. Use draft_brand to FILL it and update_brand to edit it. Re-running with the same name returns the existing profile instead of a duplicate. Creating it is free. Drafting a NEW brand into it (draft_brand) needs the credits for its research on the balance, about 100; with fewer, draft_brand refuses before anything runs or is charged and says how many it needs and how many the account has.
| Argument | Type | Description |
|---|---|---|
| name* | string | the name for the new profile (a brand, client or creator name) |
| activate | boolean | switch this connection to the new profile (default true); everything you do next scopes to it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the name for the new profile (a brand, client or creator name)"
},
"activate": {
"type": "boolean",
"description": "switch this connection to the new profile (default true); everything you do next scopes to it"
}
}
}Create a new Google Doc in the user’s Drive with a title + optional body text — e.g. export ad copy, a creative brief, or a report. Returns the document id + URL. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs).
| Argument | Type | Description |
|---|---|---|
| text | string | body text to insert |
| title | string | document title |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"text": {
"type": "string",
"description": "body text to insert"
},
"title": {
"type": "string",
"description": "document title"
}
}
}Create a folder in the user’s Google Drive (optionally nested under parentId) to organize saved files. Returns the folder id + webViewLink. Use that ID as update_drive_file’s moveToFolderId or as parentId for a nested folder. NOTE: save_to_drive’s `folder` is a NAME, not this id — it find-or-creates a folder by that name, so pass the folder NAME there (or omit and just save, then move with update_drive_file).
| Argument | Type | Description |
|---|---|---|
| name* | string | folder name |
| parentId | string | parent folder id for a nested folder (default: Drive root) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "folder name"
},
"parentId": {
"type": "string",
"description": "parent folder id for a nested folder (default: Drive root)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| path1 | string | SEARCH only — display-URL path segment, ≤15 chars |
| path2 | string | SEARCH only — second display-URL path segment, ≤15 chars |
| dryRun | boolean | true = validate with Google and create nothing |
| status | string | 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 |
| adGroupId* |
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.
| Argument | Type | Description |
|---|---|---|
| ad | object | build the ad in the same atomic call |
| name* | string | ad group name |
| dryRun | boolean | true = validate with Google and create nothing |
| status | string | 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 |
| keywords | array | a SEARCH ad group with no keywords never shows |
| cpcBidUsd | number |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name |
| dryRun | boolean | validate the WHOLE tree against Google without creating anything. Nothing is written and no budget is consumed |
| adGroup | object | build the serving tree in the same atomic call — WITHOUT this you get a campaign shell that can never show an ad |
| bidding | object | how the campaign bids — default MANUAL_CPC |
| endDate | string | YYYY-MM-DD |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | what the user calls this result, e.g. "Purchase", "Demo request" |
| type | string | default WEBPAGE — a conversion that happens on the website |
| dryRun | boolean | validate against Google and create NOTHING |
| status | string | default ENABLED — anything else records nothing |
| category | string | 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 |
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.
| Argument | Type | Description |
|---|---|---|
| ad* | object | video ad: {videoAssets or youtubeVideoIds, logo, finalUrl, businessName, headlines, longHeadlines, descriptions} |
| name* | string | campaign name |
| dryRun | boolean | true = validate with Google and create nothing |
| bidding | object | bidding object {strategy, targetCpaUsd, targetRoas, maxCpcUsd}; strategy defaults to MAXIMIZE_CONVERSIONS |
| endDate | string | campaign end date, YYYY-MM-DD |
| channels | array | omit for all of Google's channels |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string |
Create a contact in the brand's HubSpot by email, or UPDATE the contact that already has that email — HubSpot's duplicate answer carries the existing id, so this is an UPSERT and never makes a twin. Optional firstname, lastname, phone, company, website, lifecyclestage, hs_lead_status, hubspotOwnerId (the sales rep to assign it to — an owner id from list_hubspot_owners), plus any other writable contact property in `properties` (by internal name). The contact is READ BACK after the write; the reply says created or updated. Free. Needs HubSpot connected.
| Argument | Type | Description |
|---|---|---|
| email* | string | the contact email — the key HubSpot de-duplicates on |
| phone | string | contact phone number |
| company | string | company name on the contact |
| ownerId | string | older name for hubspotOwnerId (MCP only) |
| website | string | company website URL |
| lastname | string | contact last name |
| firstname | string | contact first name |
| properties | object |
Create a LinkedIn campaign inside an existing campaign group. Created DRAFT — it spends NOTHING until activated with set_linkedin_ads_status(confirm:true) — and a campaign on its own carries no creative, so it cannot serve an impression. Budget amounts are in the ad account’s currency; tell the user that LinkedIn may spend UP TO 150% of a daily budget on a high-opportunity day before they pick a number. Two LinkedIn behaviours to repeat rather than hide: on manual, target-cost or cost-cap bidding a unitCost of 0 means the campaign never delivers, and LinkedIn DEFERS some validation on DRAFT objects, so a clean create can still fail at activation — never promise it will run. TARGETING IS MANDATORY on LinkedIn — a campaign with no audience is refused outright — so pass locations (and optionally include/exclude facets like titles, industries, seniorities or staffCountRanges), or a raw targetingCriteria. Resolve every targeting value with search_linkedin_ads_targeting first: they are opaque URNs and MUST NOT be invented. LinkedIn’s own enums for type, objectiveType and costType are passed straight through, and LinkedIn’s refusal is surfaced verbatim if one is wrong. Read back before you are told it exists.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name |
| type | string | LinkedIn campaign type, e.g. SPONSORED_UPDATES |
| format | string | LinkedIn creative format value, passed through to LinkedIn |
| locale | object | LinkedIn locale object (country, language); leave unset for the default en/US |
Create a LinkedIn CAMPAIGN GROUP — the container LinkedIn has required every campaign to live inside since 2020. Created DRAFT, which is LinkedIn’s own structural safety net: it REFUSES to hold an ACTIVE campaign inside a DRAFT group, so while the group is a draft nothing beneath it can serve whatever its own status says. Creating it ACTIVE removes that protection and therefore requires confirm:true. LinkedIn REQUIRES a run schedule on a campaign group: it starts today unless you pass startDate, and if you set totalBudget you MUST also pass endDate — that pairing is LinkedIn’s own rule and it is refused here before anything is created. Read back from LinkedIn before you are told it exists.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign group name |
| status | string | default DRAFT |
| confirm | boolean | REQUIRED true to create it ACTIVE |
| endDate | string | YYYY-MM-DD — REQUIRED by LinkedIn whenever totalBudget is set |
| startDate | string | YYYY-MM-DD — when campaigns under this group may start; defaults to today |
| adAccountId | string | LinkedIn ad account id from list_linkedin_ads_campaigns |
| runSchedule | object | LinkedIn raw runSchedule object, passed through; startDate/endDate are easier |
Create the AD ITSELF on LinkedIn — a CREATIVE inside an existing campaign. A LinkedIn campaign holds no copy and no visual, so until this runs the campaign CANNOT show an impression no matter what its status says; say that rather than calling a campaign "live". Two ways in: pass postUrn to sponsor a post that already exists (LinkedIn’s "boost this post"), or pass text and/or imageUrl / videoUrl / documentUrl (a PDF/PPTX/DOCX document ad) to author a DIRECT SPONSORED CONTENT post — a real post by a company Page the user administers that is NEVER shown on the Page’s feed and exists only as an ad. Created DRAFT: it spends nothing until you activate it with set_linkedin_ads_status(level:"creative", status:"ACTIVE", confirm:true). Creating it ACTIVE under an already-live campaign starts REAL AD SPEND on the very next auction and therefore requires confirm:true. LEAD GEN ADS: under a LEAD_GENERATION campaign pass leadFormId (create_linkedin_lead_form makes one) — LinkedIn requires it there. The whole tree — group, campaign and every creative — is read back from LinkedIn before you are told anything exists.
| Argument | Type | Description |
|---|---|---|
| name | string | the creative’s name in Campaign Manager |
| text | string | the ad copy |
| title | string | title of an uploaded document or video ad (max 200 characters) |
| altText | string | image alt text for accessibility, for a single-image ad |
| confirm | boolean | REQUIRED true to create it ACTIVE under a LIVE campaign (real spend) |
Create a LinkedIn CONVERSION RULE — the object LinkedIn attributes conversions to, and the prerequisite for send_linkedin_conversions. `type` is the behaviour being tracked (LEAD, PURCHASE, SIGN_UP, QUALIFIED_LEAD, KEY_PAGE_VIEW…). THE RULE IS BORN ASSOCIATED WITH NOTHING: until you attach campaigns with associate_linkedin_conversion_campaigns it attributes nothing, which also makes it the safe place to send test events — LinkedIn has no test mode on the wire. Pass associateAllCampaigns:true to attach it to up to 200 ACTIVE campaigns instead; that spends nothing but it changes what those live campaigns optimise toward and what their reports count, so ask the user first. Attribution windows are 1, 7, 30 or 90 days (365 only for SUBMIT_APPLICATION, PURCHASE, ADD_TO_CART, QUALIFIED_LEAD and LEAD). Creating a rule cannot spend money — it is a definition. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | shown in Campaign Manager and in every report |
| type* | string | the conversion behaviour, e.g. LEAD, PURCHASE, SIGN_UP, QUALIFIED_LEAD, KEY_PAGE_VIEW |
| enabled | boolean | default true. A disabled rule REFUSES streamed events. |
| valueType | string | DYNAMIC (default) takes each event’s own value |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
| attributionType | string |
Create a LinkedIn LEAD GEN FORM — the form a lead gen ad opens, prefilled from the member’s LinkedIn profile so they submit in two taps. Owned by an ad account (adAccountId, the usual choice for ads) or a company Page (pageId). Give it a name (only shown in Campaign Manager), the headline people see, the questions (predefined fields LinkedIn prefills, plus custom text or multiple-choice questions), a privacy policy URL, optional consent checkboxes, a thank-you message and a thank-you button with its landing page. Created PUBLISHED by default — LinkedIn still lets you edit every field until a running ad uses it. Read back from LinkedIn before you are told it exists. Then attach it with create_linkedin_ads_creative(leadFormId) under a LEAD_GENERATION campaign. A form cannot spend money. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | internal name, shown in Campaign Manager |
| state | string | default PUBLISHED |
| locale | string | default en_US |
| pageId | string | Or own it by a company Page |
| consents | array | consent checkbox texts, or {text, required:true} |
| headline* | string | the title of the form people see |
| questions* | array |
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.
| Argument | Type | Description |
|---|---|---|
| countries | array | CLDR territory codes the items may be shown in |
| feedLabel | string | market bucket, e.g. "US" — max 20 chars, A-Z/0-9/dashes. IMMUTABLE, and must be set together with contentLanguage |
| displayName* | string | the name the merchant will see in Merchant Center |
| destinations | array | omit to inherit the account's own program participation (SHOPPING_ADS, FREE_LISTINGS, …) |
| contentLanguage | string | two-letter ISO 639-1, e.g. "en". IMMUTABLE, and must be set together with feedLabel |
| merchantCenterId* | string | from list_merchant_accounts |
PUT A SALE BADGE ON THE MERCHANT'S SHOPPING LISTINGS — the most seasonal thing a retail advertiser does. It creates the promotion and resolves (or creates) the promotion DATA SOURCE that country/language needs automatically; a promotion data source is its own kind and cannot be the product feed. IT IS NOT LIVE ON CREATE: Google validates promotions asynchronously and can take days to approve one, so report it as SUBMITTED and re-read with list_merchant_promotions. Everything Google marks Required is refused up front by name, including the pairings its own 400 does not mention — PERCENT_OFF needs percentOff, MONEY_OFF needs moneyOffAmount, GENERIC_CODE needs genericRedemptionCode — because otherwise Google accepts the promotion and disapproves it hours later where nobody is watching. 0 credits.
| Argument | Type | Description |
|---|---|---|
| endDate* | string | ISO date; widened to the END of that day, because Google's interval end is exclusive |
| longTitle* | string | the promotion text shoppers see, e.g. '20% off all winter coats' — describe the offer, do not name the campaign |
| offerType* | string | NO_CODE when the discount applies automatically, GENERIC_CODE when the shopper types a code |
| startDate* | string | ISO date, e.g. 2026-11-27 |
| attributes | object | any other promotion attribute Google publishes — item/brand/product-type filters, minimum purchase, store filters, display period — forwarded as given |
Build a complete, ready-to-run Meta ad: campaign -> ad set (FULL targeting + budget + schedule + bidding) -> creative -> ad(s), ALL created PAUSED — it spends NOTHING until you activate the campaign with set_meta_campaign_status(confirm:true). This is the "create a campaign and put the ads on it" path. IMAGE, VIDEO (uploaded, transcoded and thumbnailed for you) and CAROUSEL (format:"carousel", 2–10 cards each with its own headline/description/link) all work. Targeting is the `targeting` object: geo down to cities with a radius, age, gender, interests, behaviours, custom audiences and lookalikes, languages, placements, devices and OS. For a conversion objective pass pixelId + conversionEvent and the ad set optimizes for that conversion. Schedule with startTime/endTime + dayparting; bid with bidStrategy + bidAmountUsd/minRoas; use lifetimeBudgetUsd (with endTime) for a fixed flight. Attach to an existing campaign with campaignId or an existing ad set with adSetId. Everything is READ BACK from Meta before you are told it exists — print the returned summary verbatim (it carries 24-hour Meta PREVIEW LINKS for the first ad — hand them to the user; preview_meta_ad renders any ad in any placement). Needs ads-management on the connected account. BOOST AN EXISTING POST: pass boostPostId — a post you have ALREADY published (numeric id, the <pageId>_<postId> form, or a permalink) — INSTEAD of any creative, and the ad promotes that post exactly as published, comments and all. Meta ignores creative overrides on an existing post, so message/headline/cta/link do NOT apply; targeting, budget, schedule, bidding and PAUSED-by-default all work identically. AN INSTAGRAM POST NEEDS boostTarget:"instagram" — IG media ids and Facebook post ids are both bare digits, so Hermoso never guesses, and a Facebook boost given an IG media id is refused. Instagram eligibility is checked for free before anything is created (Meta refuses to boost a post carrying licensed music or an interactive element).
| Argument | Type | Description |
|---|---|---|
| cta | string | call-to-action, e.g. SHOP_NOW / LEARN_MORE / SIGN_UP (default LEARN_MORE); WHATSAPP_MESSAGE for a click-to-WhatsApp ad (see destinationType). Any Meta call-to-action type works (SEE_DETAILS…); unknown ones are refused, not swapped. |
Create an AD SET on an EXISTING Meta campaign — the level that holds the audience, budget, schedule and bidding. Use it to hang SEVERAL ad sets off ONE campaign, which is how you actually test audiences on Meta (one ad set per audience, same campaign, same creative). Takes the same full `targeting`, pixelId/conversionEvent, bidStrategy, schedule and budget fields as create_meta_ad. Created PAUSED and read back from Meta. It has NO ads until you call create_meta_ad(adSetId:…). For CATALOG / ADVANTAGE+ CATALOG ads (DPA), pass productSetId here — the campaign carries productCatalogId and the ad set narrows it to a product set.
| Argument | Type | Description |
|---|---|---|
| name | string | ad set name |
| pageId | string | Omit when the brand has one Page; with several the call lists them, so ask the user which one. |
| country | string | 2-letter shorthand when you are not passing full targeting (default US) |
| endTime | string | REQUIRED with lifetimeBudgetUsd |
| minRoas | number | REQUIRED for LOWEST_COST_WITH_MIN_ROAS, e.g. 1.1 |
| pixelId | string | Meta Pixel id — with this the ad set optimizes for a real CONVERSION instead of falling back to link clicks |
| dsaPayor | string |
Build a retargeting audience on a connected Meta ad account. Four kinds: kind:"website" (people whose visited URL contains urlContains, seen by pixelId — pass the brand’s own domain for "all visitors"; retentionDays up to 180), kind:"engagement" (people who did `event` on the brand’s Facebook Page, or its Instagram business profile with source:"instagram"; retentionDays up to 730), or kind:"lookalike" (sourceAudienceId + country + ratio 0.01–0.20 = the top 1%–20% most similar people, lookalikeType "similarity" or "reach", or startingRatio for a band such as 1%–3%). Meta delivers a lookalike to the locations of the AD SET that targets it, so `country` is where you will run it — target the same country in that ad set. CREATING AN AUDIENCE SPENDS NOTHING — it is a definition; money only moves when an ad set targets it and that campaign is activated through set_meta_campaign_status(confirm:true). Meta needs roughly 30 minutes and ~1,000 people before a new audience can be targeted, so a fresh one reporting no size is normal. kind:"customerfile" is the brand's OWN CUSTOMER LIST (emails, phones, names…) for retargeting, suppression or as a lookalike seed: pass customerFileSource (a required attestation of where the data came from: USER_PROVIDED_ONLY, PARTNER_PROVIDED_ONLY or BOTH_USER_AND_PARTNER_PROVIDED), plus schema (the column names in order, from EMAIL, PHONE, FN, LN, FI, CT, ST, ZIP, COUNTRY, DOB, DOBY, GEN, MADID, EXTERN_ID) and rows (arrays in that order, up to 10,000 per call) to upload them in the same call. Hermoso normalizes and SHA-256-hashes every value before it leaves the server, exactly as Meta requires, and reports Meta’s own match counts (a mostly-rejected upload is a format problem and says so). To ADD more people to an existing list, call again with kind:"customerfile" + audienceId + schema + rows (uploads add, they never replace). Meta asks the ad account to accept its Custom Audience Terms once; if it has not, Meta’s refusal says so.
| Argument | Type | Description |
|---|---|---|
| kind* | string | which kind of audience to build — customerfile = your own customer list |
| name |
Use this when the user asks to create a new Facebook or Instagram ad campaign on their own ad account. Create a campaign on a connected Meta ad account. Always created PAUSED — it spends NOTHING until you activate it with set_meta_campaign_status(confirm:true). Optionally set a dailyBudgetUsd (an amount in the AD ACCOUNT’s currency, e.g. CAD on a CAD account — the name is historical, it is not USD). Pass adAccountId + an objective. Needs ads-management permission on the connected account. CATALOG / ADVANTAGE+ CATALOG ADS (also called DPA or dynamic product ads) ARE SUPPORTED: pass productCatalogId here and productSetId on the ad set, and the campaign is catalog-backed. Say YES when a user asks for catalog, Advantage+ catalog, DPA or feed-driven ads.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name |
| objective | string | default OUTCOME_TRAFFIC |
| adAccountId* | string | Ad account id |
| bidStrategy | string | with dailyBudgetUsd: how the campaign bids across its ad sets — LOWEST_COST_WITHOUT_CAP (default: highest volume), LOWEST_COST_WITH_BID_CAP / COST_CAP (each ad set then needs bidAmountUsd), LOWEST_COST_WITH_MIN_ROAS (each ad set needs minRoas) |
| dailyBudgetUsd | number | optional campaign daily budget in the AD ACCOUNT's currency (not necessarily USD — the reply names it); real spend once ACTIVE. With it, the campaign holds the budget (Advantage+ campaign budget) and every ad set under it carries NONE — create_meta_adset / create_meta_ad then send no ad-set budget |
Create an INSTANT LEAD FORM on a connected Facebook Page — the in-app form a Meta lead ad opens instead of sending someone to a website, so a person can sign up without leaving the app. `questions` takes Meta’s own type names, e.g. ["FULL_NAME","EMAIL","PHONE"]; for anything bespoke pass {type:"CUSTOM", label:"What size fleet do you run?"} and add options:[…] to make it a dropdown. Ask FEWER questions than you think — every extra field costs completions. privacyPolicyUrl is REQUIRED: the form collects real people’s contact details and has to say where their data goes. Optional: headline, contextCard {title, bullets[], buttonText} for the why-should-I screen, thankYou {title, body, buttonType, buttonText, websiteUrl}, locale, followUpActionUrl. CREATING A FORM SPENDS NOTHING and publishes nothing — it is invisible to the public until an ad points at it (create_meta_ad with objective:"OUTCOME_LEADS" + leadFormId), and that ad is created PAUSED. Read the submissions with read_meta_leads. NEEDS pages_manage_ads — see list_meta_lead_forms for what to do if Meta refuses it.
| Argument | Type | Description |
|---|---|---|
| name* | string | internal name for the form — not shown to the person filling it in |
| locale | string | form language, e.g. EN_US, ES_ES, PT_BR, DE_DE |
| pageId | string | which connected Page (required only if the profile has more than one) |
| headline | string | headline shown above the questions |
| thankYou | object | {title, body, buttonType, buttonText, websiteUrl} — shown after submitting |
Create a META PIXEL on one of the brand’s ad accounts, so their ads can optimise for real website conversions instead of link clicks and so website retargeting audiences become possible. A pixel is a DEFINITION: it cannot serve, cannot spend, and RECORDS NOTHING until its snippet is installed on the site — the reply hands back that snippet and says so. TWO THINGS TO SAY OUT LOUD BEFORE CALLING IT: Meta allows exactly ONE pixel per ad account (a second attempt is refused and Hermoso will name the one that already exists — so call list_meta_pixels first), and Meta publishes NO WAY TO DELETE a pixel, so this is permanent. Creating it spends nothing.
| Argument | Type | Description |
|---|---|---|
| name* | string | what it will be called in Events Manager — name it after the website or brand it measures |
| adAccountId* | string | The ad account, with or without the act_ prefix |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "what it will be called in Events Manager — name it after the website or brand it measures"
},
"adAccountId": {
"type": "string",
"description": "The ad account, with or without the act_ prefix"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| path1 | string | display-URL path segment, ≤15 chars, no "/" |
| path2 | string | second display-URL path segment (only with path1) |
| status | string | default Paused |
| confirm | boolean | REQUIRED true to create this Active in a LIVE ad group |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId* | string | the ad group this ad lives in |
| finalUrls | array | the landing page(s) — at least one is required |
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.
| Argument | Type | Description |
|---|---|---|
| ad | object | create the responsive search ad in the same call |
| name* | string | ad group name, ≤256 characters |
| cpcBid | number | default max CPC in the account currency |
| status | string | default Paused |
| confirm | boolean | REQUIRED true to create this Active under a LIVE campaign |
| keywords | array | keywords to add: [{text, matchType, bid, status}]; status defaults to Paused |
| language | string | required if the campaign has no language set |
| accountId |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name, ≤128 characters |
| maxCpc |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | ≤100 characters, unique per customer |
| type* | string | Url (thank-you page), Event (UET custom event), OfflineConversion, Duration or PagesViewedPerVisit |
| scope | string | Account = this ad account only; Customer = shared across the customer |
| tagId | string | UET tag id from create_microsoft_ads_uet_tag; not needed for OfflineConversion |
| revenue | number | FixedValue: the value of one conversion; VariableValue: the default |
| category | string | conversion category, e.g. Purchase or Signup; required for Url, Event and OfflineConversion |
| accountId | string |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | maximum 128 characters |
| scope | string | Account (default) = this ad account only; Customer = every account under the manager |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| description | string | maximum 1024 characters |
| membershipDuration | number | 1–390 days, or -1 for no expiration. Microsoft’s default is 30 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "maximum 128 characters"
},
"scope": {
"enum": [
"Account",
"Customer"
],
"type": "string",
"description": "Account (default) = this ad account only; Customer = every account under the manager"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"description": {
"type": "string",
"description": "maximum 1024 characters"
},
"membershipDuration": {
"type": "number",
"description": "1–390 days, or -1 for no expiration. Microsoft’s default is 30"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| images | object | pictures by shape: {landscape, square, logo} URLs, instead of imageUrl |
| status | string | default Paused |
| confirm | boolean | true required to create the ad Active in a live ad group |
| logoUrl | string | logo picture URL |
| imageUrl | string | one picture, cut to both shapes for you |
| videoIds | array | video ids from upload_microsoft_ads_video to attach |
| accountId | string |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | tag name, up to 100 characters, unique per customer |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| description | string | optional note on what the tag tracks |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "tag name, up to 100 characters, unique per customer"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"description": {
"type": "string",
"description": "optional note on what the tag tracks"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| date | string | "YYYY-MM-DD HH:mm:ss", "YYYY-MM-DD" or ISO with offset; default now |
| tags | array | annotation-tag ids |
| description* | string | the text shown on the chart, e.g. "Meta: summer UGC campaign went live" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"description"
],
"properties": {
"date": {
"type": "string",
"description": "\"YYYY-MM-DD HH:mm:ss\", \"YYYY-MM-DD\" or ISO with offset; default now"
},
"tags": {
"type": "array",
"items": {
"type": "number"
},
"description": "annotation-tag ids"
},
"description": {
"type": "string",
"description": "the text shown on the chart, e.g. \"Meta: summer UGC campaign went live\""
}
}
}Create a folder in the user’s OneDrive (optionally nested under parentId) to organize saved files. Returns the folder id + webViewLink. Use that id as update_onedrive_file’s moveToFolderId or as parentId for a nested folder. NOTE: save_to_onedrive’s `folder` is a NAME (find-or-created), not this id.
| Argument | Type | Description |
|---|---|---|
| name* | string | folder name |
| parentId | string | parent folder id for a nested folder (default: OneDrive root) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "folder name"
},
"parentId": {
"type": "string",
"description": "parent folder id for a nested folder (default: OneDrive root)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name | string | internal name — defaults to the title |
| status | string | default paused |
| confirm | boolean | REQUIRED true to create this ACTIVE in a live ad group (real spend) |
| creative* | object | title 3-50 characters, body up to 100, landing page URL and one still image; no video |
| adGroupId |
Add an ad group to an existing ChatGPT Ads campaign. Created PAUSED by default. Bidding: leave it out and the ad group gets INTELLIGENT BIDDING (OpenAI’s renamed “Maximize results”: maximize_clicks / maximize_conversions, no amount; needs a daily campaign budget). From 2026-11-09: bidStrategy target_cpa + targetCpa (conversions campaigns) or target_cpc + targetCpc (clicks campaigns). A fixed maxBid is Max Bid / Max CPC, deprecated and retired by ChatGPT Ads on 2027-01-01. The reply says if ChatGPT Ads stored a different bid than was sent. 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.
| Argument | Type | Description |
|---|---|---|
| name* | string | ad group name |
| maxBid | number | DEPRECATED — Max Bid / Max CPC, retired by ChatGPT Ads on 2027-01-01: a fixed bid in the account currency (bidStrategy fixed_bid; on a conversions campaign it is the CPA bid). Needed only on an impressions campaign (per impression); otherwise use intelligent bidding or a target. |
| status | string | default paused |
| confirm | boolean |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | audience name |
| fileUrl | string | public link to a UTF-8 .csv or .txt customer list; upload_file turns a local file into one |
| members | array | emails and/or phone numbers (already-SHA256-hashed emails are passed through as-is); or use fileUrl |
| fileName | string | the file name with .csv or .txt, when the link hides it |
| description | string | optional audience description |
| identifierType | string | the one identifier the file holds (inferred from a CSV header when omitted) |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | business agent name |
| toolIds | array | tool ids the agent may use, from list_openai_ads_business_agent_tools |
| leadFormId | string | published lead form id the agent collects leads with |
| description | string | optional description of the agent |
| connectorIds | array | connector ids the agent may use |
| instructions* | string | system prompt that users experience, written in the brand's voice |
| productFeedIds | array | product feed ids the agent may draw on, from list_openai_ads_feeds |
Build a campaign on the connected ChatGPT Ads account — the ads that appear below ChatGPT answers. ALWAYS created PAUSED at every level, with no override: it spends NOTHING until you activate it with set_openai_ads_status(confirm:true). The object graph is campaign -> ad group -> ad, and a campaign ON ITS OWN CANNOT SERVE AN IMPRESSION, so pass adGroup{name, contextHints, ad{creative}} and this builds the whole tree. BIDDING: leave the ad group’s bid out and it gets INTELLIGENT BIDDING (maximize_clicks / maximize_conversions, OpenAI’s renamed “Maximize results”); from 2026-11-09 a cost target is available (target_cpa + targetCpa, target_cpc + targetCpc); a fixed maxBid (Max Bid / Max CPC) is deprecated and ChatGPT Ads retires it on 2027-01-01. 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. 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.
| Argument | Type | Description |
|---|---|---|
| name* |
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.
| Argument | Type | Description |
|---|---|---|
| name* | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "a descriptive name, 3–1000 characters — it can never be listed or renamed afterwards"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — the key is permanent and its secret is shown once"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | display name of the conversion event |
| eventType* | string | e.g. order_created, lead_created, registration_completed — the plausible words "purchase", "lead" and "signup" are all REFUSED by ChatGPT Ads |
| sourceIds* | array | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"eventType",
"sourceIds"
],
"properties": {
"name": {
"type": "string",
"description": "display name of the conversion event"
},
"eventType": {
"type": "string",
"description": "e.g. order_created, lead_created, registration_completed — the plausible words \"purchase\", \"lead\" and \"signup\" are all REFUSED by ChatGPT Ads"
},
"sourceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "exactly ONE source id from create_openai_ads_pixel (ChatGPT Ads takes one source per event; create one event per pixel)"
},
"customEventName": {
"type": "string",
"description": "for a non-standard event"
},
"attributionWindowDays": {
"type": "number",
"description": "default 30, which is what OpenAI recommends; 1-90"
}
}
}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).
| Argument | Type | Description |
|---|---|---|
| name* | string | product feed name |
| countries | array | ISO country codes the feed serves, e.g. US |
| inventoryType | string | default product; hotel = a Hotel property feed (limited beta) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "product feed name"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO country codes the feed serves, e.g. US"
},
"inventoryType": {
"enum": [
"product",
"hotel"
],
"type": "string",
"description": "default product; hotel = a Hotel property feed (limited beta)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | lead form name |
| fields* | array | form questions, in the order shown |
| privacyPolicyUrl | string | privacy policy URL shown with the form |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"fields"
],
"properties": {
"name": {
"type": "string",
"description": "lead form name"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"required": [
"fieldId",
"fieldType",
"label"
],
"properties": {
"label": {
"type": "string",
"description": "question text shown to the person"
},
"fieldId": {
"type": "string",
"description": "your stable key for the field, e.g. \"email\""
},
"options": {
"type": "array",
"items": {
"type": "string"
},
"description": "choice fields only"
},
"required": {
"type": "boolean",
"description": "true = the person must answer this question"
},
"fieldType": {
"enum": [
"text",
"choice"
],
"type": "string",
"description": "text, or choice (choice needs options)"
}
}
},
"description": "form questions, in the order shown"
},
"privacyPolicyUrl": {
"type": "string",
"description": "privacy policy URL shown with the form"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "a name for the pixel"
},
"automaticAdvancedMatching": {
"type": "boolean",
"description": "default true (OpenAI’s own default). false stops the Pixel collecting and hashing customer information from the page."
}
}
}Create the actual AD inside a Pinterest ad group — this is the object that carries the creative; a campaign or ad group alone shows nothing. A Pinterest ad PROMOTES AN EXISTING PIN, so pass pinId. Created PAUSED; if the parent ad group and campaign are both ACTIVE, creating this ACTIVE starts REAL AD SPEND immediately — get an explicit yes and pass confirm:true. Pinterest reviews ads: the read-back reports the review status and any rejection reason, and a REJECTED ad never serves until it is fixed.
| Argument | Type | Description |
|---|---|---|
| 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 |
| name | string | internal ad name |
| pinId* | string | The numeric id of an existing Pin |
| status | string | default PAUSED |
| confirm | boolean | REQUIRED true to create this ACTIVE in a live ad group |
| adGroupId* | string | the ad group this ad lives in |
| adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
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.
| Argument | Type | Description |
|---|---|---|
| ad | object | create the ad in the same call |
| 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. |
| name* | string | ad group name |
| budget | number | ad-group budget — only valid when the campaign is NOT budget-optimized (Pinterest optimizes at campaign level by default) |
| pacing | string | delivery speed: STANDARD or ACCELERATED |
| status | string | default PAUSED |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | audience name |
| rule | object | raw Pinterest rule fields, merged last — expert use |
| event | string | VISITOR: pagevisit, signup, checkout, viewcategory, search, addtocart, watchvideo, lead, custom (or a partner-defined tag event) |
| maids | array | CUSTOMER_LIST: mobile ad ids to upload as a new list, hashed on our server |
| emails | array | CUSTOMER_LIST: upload these as a new list (hashed here) and make the audience in one call |
| pinIds | array | ENGAGEMENT: only people who engaged with these Pins |
| country |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name, ≤255 characters |
| adGroup | object | build the serving tree in the same call — WITHOUT this you get a campaign shell that can never show an ad |
| endTime | number | Unix timestamp in SECONDS |
| objective* | string | Pinterest requires an objective and will not guess one |
| startTime |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | tag name, as shown in Pinterest Ads Manager |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| enhancedMatch | any | true enables all Automatic Enhanced Match fields; or an object to enable individual fields |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "tag name, as shown in Pinterest Ads Manager"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"enhancedMatch": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "object",
"properties": {
"name": {
"type": "boolean"
},
"email": {
"type": "boolean"
},
"phone": {
"type": "boolean"
},
"gender": {
"type": "boolean"
},
"location": {
"type": "boolean"
},
"birthdate": {
"type": "boolean"
},
"externalId": {
"type": "boolean"
}
}
}
],
"description": "true enables all Automatic Enhanced Match fields; or an object to enable individual fields"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | customer list name |
| maids | array | mobile ad ids to upload; hashed on our server before sending |
| emails | array | email addresses to upload; hashed on our server before sending |
| listType | string | IDFA or MAID for mobile ids; ignored when emails are sent (list becomes EMAIL) |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| audienceName | string | name of the CUSTOMER_LIST audience made from this list (when createAudience is on) |
| createAudience | boolean | default true |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "customer list name"
},
"maids": {
"type": "array",
"items": {
"type": "string"
},
"description": "mobile ad ids to upload; hashed on our server before sending"
},
"emails": {
"type": "array",
"items": {
"type": "string"
},
"description": "email addresses to upload; hashed on our server before sending"
},
"listType": {
"enum": [
"EMAIL",
"MAID",
"IDFA"
],
"type": "string",
"description": "IDFA or MAID for mobile ids; ignored when emails are sent (list becomes EMAIL)"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"audienceName": {
"type": "string",
"description": "name of the CUSTOMER_LIST audience made from this list (when createAudience is on)"
},
"createAudience": {
"type": "boolean",
"description": "default true"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| bid | number | bid in the ad account currency |
| cta | string | catalog sales CTAs: SHOP_NOW, BOOK_NOW, ON_SALE, GET_DEAL |
| status | string | default PAUSED |
| confirm | boolean | true required to create ACTIVE promotions under a live campaign and ad group |
| included | boolean | false excludes these product groups instead of including them |
| adGroupId* | string | CATALOG_SALES ad group to attach the product groups to |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
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.
| Argument | Type | Description |
|---|---|---|
| scope | string | default project. PostHog's `recording` scope is deprecated and rejected by PostHog |
| content* | string | the text shown on the chart, ≤8192 characters |
| insightId | number | REQUIRED when scope is "dashboard_item" — the numeric id of the insight |
| dateMarker | string | ISO 8601 — defaults to now, which is right for "this just went live" |
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.
| Argument | Type | Description |
|---|---|---|
| name* | 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" |
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.
| Argument | Type | Description |
|---|---|---|
| name* | 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 |
| bidType | string | required — must fit the campaign objective, CLICKS campaigns take CPC; under a CBO campaign it is taken from the campaign |
| endTime | string | ISO 8601 end time; omit for no end |
| goalType |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | what this list is, e.g. "Purchasers – last 180 days" |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "what this list is, e.g. \"Purchasers – last 180 days\""
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name |
| appId | string | APP_INSTALLS only: the App Store id (e.g. 1064216828) or Google Play package (e.g. com.reddit.frontpage) |
| budget | number | CBO only: the campaign budget in the ad account’s currency, paired with goalType |
| bidType | string | CBO only, must fit the objective — CLICKS takes CPC |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | catalog name |
| pixelIds | array | Reddit pixel ids from list_reddit_ads_pixels to match products against; each must be on the shared ad account |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| defaultCurrency* | string | the currency your product prices are in — fixed once created |
| defaultLanguage | string | default en — fixed once created |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | internal name — redditors do not see it |
| prompt* | string | the line shown above the form telling people what they are signing up for |
| questions* | array |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name, 3 to 500 characters |
| appId | string | App Store or Google Play id; required for APP_INSTALLS |
| adName | string | name of the template ad |
| budget* | number | campaign budget in the ad account’s currency (a Max budget lives on the campaign) |
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).
| Argument | Type | Description |
|---|---|---|
| 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. |
| type | string | default TEXT |
| 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. |
| media | array | one entry for IMAGE/VIDEO, up to 6 for CAROUSEL |
| headline* |
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.
| Argument | Type | Description |
|---|---|---|
| url* | string | the product file Reddit fetches |
| mode | string | default REPLACE — the file is the whole catalog |
| name | string | feed name, for your reference |
| 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 |
| username | string | only if the url needs a login |
| catalogId* | string |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | product set name |
| filter* | any | the rule as an object (or a JSON string), e.g. {"availability":{"eq":"IN_STOCK"}} |
| catalogId* | string | catalog the product set belongs to |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId",
"name",
"filter"
],
"properties": {
"name": {
"type": "string",
"description": "product set name"
},
"filter": {
"description": "the rule as an object (or a JSON string), e.g. {\"availability\":{\"eq\":\"IN_STOCK\"}}"
},
"catalogId": {
"type": "string",
"description": "catalog the product set belongs to"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | saved audience name |
| targeting* | object | same shape as create_reddit_ads_ad_group targeting — an empty block is refused, because a saved audience IS its targeting |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"targeting"
],
"properties": {
"name": {
"type": "string",
"description": "saved audience name"
},
"targeting": {
"type": "object",
"description": "same shape as create_reddit_ads_ad_group targeting — an empty block is refused, because a saved audience IS its targeting",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}Create a new Google Spreadsheet in the user’s Drive and optionally fill it with rows — e.g. export a swipefile, ad list, or performance report. Pass rows as an array of row arrays (first row = headers). Returns the spreadsheet id + URL. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs).
| Argument | Type | Description |
|---|---|---|
| rows | array | rows to write — array of row arrays; first row = headers |
| title | string | spreadsheet title |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"rows": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "rows to write — array of row arrays; first row = headers"
},
"title": {
"type": "string",
"description": "spreadsheet title"
}
}
}Create the Snapchat ad itself, inside an ad squad. CREATED PAUSED with no override. THE CREATIVE COMES FROM upload_snapchat_ads_creative: pass the creativeId it returns. AN AD POINTS AT A CREATIVE, NEVER AT A MEDIA ID — passing a mediaId is refused by name rather than failing at Snapchat with a field-path error. SNAPCHAT REVIEWS EVERY AD before it can show: the returned reviewStatus says whether that has happened, and an ad Snapchat has REJECTED cannot serve even once it is activated, so relay a rejection instead of reporting a successful build. The status is READ BACK from Snapchat’s own row — if the note carries a Warning: saying it was stored as anything other than PAUSED, relay that and pause it before anything above it is activated.
| Argument | Type | Description |
|---|---|---|
| name* | string | max 375 characters |
| type | string | omit it — Hermoso reads the creative and picks the ad type Snapchat pairs with it (WEB_VIEW -> REMOTE_WEBPAGE, LEAD_GENERATION -> LEAD_GENERATION, SNAP_AD -> SNAP_AD). Pass one only to override. |
| adSquadId* | string | the ad squad this ad belongs to |
| creativeId* | string | from upload_snapchat_ads_creative — NOT a mediaId |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
Create an ad squad under an existing Snapchat campaign — the tier that holds the budget, the bid, the targeting, the placements and the schedule. CREATED PAUSED with no override. TWO THINGS ARE MANDATORY AND NEITHER IS GUESSABLE: countries (at least one two-letter code — Snapchat refuses an ad squad with no geo, and search_snapchat_ads_targeting(kind:"country") resolves them), and a budget. **MONEY ON SNAPCHAT IS MICRO-CURRENCY**: state a plain amount in dailyBudget (50 means fifty dollars) and Hermoso multiplies by 1,000,000 for you. Only use dailyBudgetMicro if you have ALREADY converted, and NEVER pass both — that is refused rather than resolved by precedence, because applying the conversion twice to an already-converted amount asks for a budget a million times too large, and under-converting merely fails loudly while over-converting does not. Snapchat’s documented minimum daily budget is 5 units. A bid is required unless bidStrategy is AUTO_BID, which lets Snapchat choose it.
| Argument | Type | Description |
|---|---|---|
| bid | number | in the ad account’s own currency — required unless bidStrategy is AUTO_BID |
| name* | string | max 375 characters |
| type | string | default SNAP_ADS |
| gender | string | leave unset to reach everyone — an absent gender is no gender restriction, not a default |
| maxAge | string | maximum target age as a string, e.g. '65'; omit for no upper bound |
Create a Snapchat custom audience. sourceType FIRST_PARTY: a customer list, EMPTY until update_snapchat_ads_audience_users adds people (retention defaults to lifetime). LOOKALIKE: people like a customer-list seed — seedSegmentId + countries, lookalikeType BALANCE (default) / SIMILARITY / REACH, retention max 180. PIXEL: people who fired Snap Pixel events — pixelId + events (e.g. PURCHASE, ADD_CART) + retentionInDays up to 395. ENGAGEMENT is read-only on Snapchat and refused. An audience spends nothing; it is read back from Snapchat. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | audience name in Snapchat Ads Manager |
| events | array | PIXEL: the events that put someone in it, e.g. ["PURCHASE","START_CHECKOUT"] |
| pixelId | string | PIXEL: from list_snapchat_ads_pixels |
| countries | array | LOOKALIKE: two-letter codes, e.g. ["US"] |
| sourceType | string | default FIRST_PARTY |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| description | string | audience description; optional |
| lookalikeType | string |
Create the top tier of a Snapchat ad — the campaign. CREATED PAUSED AND THERE IS NO OVERRIDE; it spends nothing until set_snapchat_ads_status(confirm:true). A CAMPAIGN ALONE CAN NEVER SERVE: it needs an ad squad and then an ad under it, and every tier must be ACTIVE before one impression is shown. Objectives are Snapchat’s CURRENT objective_v2 set — AWARENESS_AND_ENGAGEMENT, SALES, TRAFFIC, APP_PROMOTION, LEADS. A LEGACY objective name (BRAND_AWARENESS, WEB_CONVERSION and the rest) is REFUSED BY NAME rather than silently mapped onto a v2 value, because mapping one onto the other would optimise the campaign for something the user did not ask for. The result is READ BACK from Snapchat and the note says so when the read-back could not be run.
| Argument | Type | Description |
|---|---|---|
| name* | string | the campaign name in Snapchat Ads Manager — max 375 characters |
| endTime | string | ISO 8601 |
| buyModel | string | default AUCTION. RESERVED is Reach & Frequency booking and has its own contract. |
| objective | string | AWARENESS_AND_ENGAGEMENT, SALES, TRAFFIC, APP_PROMOTION or LEADS. A legacy name is refused by name. |
| startTime | string | ISO 8601 — defaults to now, which is harmless because the campaign is paused |
| adAccountId | string |
Create the in-app form a Snapchat lead ad opens. Snapchat's rules are refused BY NAME, never repaired: FIRST_NAME and LAST_NAME are required, plus at least one of EMAIL or PHONE_NUMBER; ADDRESS and POSTAL_CODE cannot both be on one form; title max 25 characters, description max 180, legal title 35 / description 80, at most 2 consent checkboxes, only one question may use preferred options, and an end page (endPage) is required. A form spends nothing; it is read back from Snapchat. Snapchat has NO endpoint that lists leads afterwards — to receive them, turn on delivery with manage_snapchat_ads_lead_webhook(action:'subscribe'). Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | the form name in Ads Manager |
| legal | object | legal disclosure: title max 35, description max 80, up to 2 consent checkboxes |
| title* | string | max 25 characters, shown at the top of the form |
| fields* | array | the questions, in order: standard types as strings (FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, ADDRESS, POSTAL_CODE, BIRTHDAY_DATE, JOB_TITLE, COMPANY_NAME) and custom questions as {question, kind, options} |
| endPage* | object | REQUIRED by Snapchat (E25022): the screen shown after someone submits |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
Create a Stripe coupon: exactly one of percentOff (1–100) or amountOff (major units) + currency; duration once (default), repeating (with durationInMonths) or forever; optional name, id and maxRedemptions. A coupon alone has NO code a customer can type: pass `code` (letters, digits, dashes) to also mint the customer-typable promotion code in the same call, with optional codeMaxRedemptions, expiresAt, firstTimeOnly and minimumAmount. Read back. LIVE key: confirm:true after showing the preview. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| id | string | custom coupon id; Stripe generates one if omitted |
| code | string | the customer-typable promotion code, e.g. SUMMER20 — letters, digits and dashes only; refused (never altered) otherwise |
| name | string | display name for the coupon, for your records |
| confirm | boolean | true on a live key, after the user approves the preview |
| currency | string | 3-letter currency code; required with amountOff |
| duration | string | once (default), repeating or forever |
| amountOff | number | fixed discount in major units, e.g. 5 = 5.00; needs currency |
| expiresAt |
Create a Stripe customer by email (optional name, phone, description, metadata). If a customer with that email already exists it is returned instead and nothing is created — pass allowDuplicate:true to create a second one on purpose. LIVE key: confirm:true after showing the preview. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| name | string | customer name |
| email* | string | customer email; the key Stripe matches duplicates on |
| phone | string | customer phone number |
| confirm | boolean | true on a live key, after the user approves the preview |
| metadata | object | key/value metadata stored on the customer |
| description | string | internal note on the customer record |
| allowDuplicate | boolean | true = create a second customer even if this email already has one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email"
],
"properties": {
"name": {
"type": "string",
"description": "customer name"
},
"email": {
"type": "string",
"description": "customer email; the key Stripe matches duplicates on"
},
"phone": {
"type": "string",
"description": "customer phone number"
},
"confirm": {
"type": "boolean",
"description": "true on a live key, after the user approves the preview"
},
"metadata": {
"type": "object",
"description": "key/value metadata stored on the customer",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": {
"type": "string",
"description": "internal note on the customer record"
},
"allowDuplicate": {
"type": "boolean",
"description": "true = create a second customer even if this email already has one"
}
}
}Create a shareable Stripe Payment Link: for ONE price pass priceId (price_…) with an optional quantity; for several pass lineItems: [{priceId, quantity}]. Optionally redirect to afterCompletionUrl when paid. allowPromotionCodes:true shows the promotion-code box at checkout — needed for any 'use code X' ad. referenceId and utm return a taggedUrl: put THAT URL in the ad, and get_stripe_payment_link_revenue (groupBy reference) then counts that ad's purchases. Returns the URL. LIVE key: confirm:true after showing the preview. Idempotent on its arguments. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| utm | object | UTM values on the taggedUrl (letters, digits, dashes, underscores, up to 150 each); Stripe passes them to the after-payment redirect only |
| confirm | boolean | true on a live key, after the user approves the preview |
| priceId | string | one price — the shorthand for a single line item |
| metadata | object | key/value metadata stored on the payment link |
| quantity | number | quantity of priceId, e.g. 2 |
| lineItems | array | several prices at once; use instead of priceId |
| referenceId | string | letters, digits, dashes, underscores (up to 200) — tags the returned taggedUrl for one ad; Stripe stores it on each purchase |
Add a price to an existing Stripe product: `amount` in MAJOR units (19 or 19.00 is $19.00, never 1900), currency, optional recurring interval and intervalCount, optional nickname. Read back. LIVE key: confirm:true after showing the preview. Idempotent on its arguments. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| amount* | number | MAJOR units — 19 or 19.00 is $19.00, never 1900 |
| confirm | boolean | true on a live key, after the user approves the preview |
| currency* | string | 3-letter ISO currency code, e.g. usd |
| interval | string | recurring interval: day, week, month or year; omit for a one-time price |
| nickname | string | internal label for the price; not shown to customers |
| productId* | string | product id, prod_..., from create_stripe_product or list_stripe_products |
| intervalCount | number | intervals between charges, e.g. 3 with month for quarterly |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"productId",
"amount",
"currency"
],
"properties": {
"amount": {
"type": "number",
"description": "MAJOR units — 19 or 19.00 is $19.00, never 1900"
},
"confirm": {
"type": "boolean",
"description": "true on a live key, after the user approves the preview"
},
"currency": {
"type": "string",
"description": "3-letter ISO currency code, e.g. usd"
},
"interval": {
"type": "string",
"description": "recurring interval: day, week, month or year; omit for a one-time price"
},
"nickname": {
"type": "string",
"description": "internal label for the price; not shown to customers"
},
"productId": {
"type": "string",
"description": "product id, prod_..., from create_stripe_product or list_stripe_products"
},
"intervalCount": {
"type": "number",
"description": "intervals between charges, e.g. 3 with month for quarterly"
}
}
}Create a product on the brand's Stripe account together with its default price: name, optional description, `amount` in MAJOR units — 19 or 19.00 both mean $19.00, and a zero-decimal currency like JPY is whole — a 3-letter currency, and an optional recurring interval (day, week, month, year) with intervalCount. Returns both ids, read back. On a LIVE key it shows what it is about to create and needs confirm:true; on a test key it just does it. Idempotent on its arguments. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| name* | string | product name |
| amount* | number | MAJOR units — 19 or 19.00 is $19.00, never 1900 |
| confirm | boolean | true on a live key, after the user approves the preview |
| currency* | string | 3-letter ISO currency code, e.g. usd |
| interval | string | recurring interval: day, week, month or year; omit for a one-time price |
| metadata | object | key/value metadata stored on the product |
| nickname | string | internal label for the default price; not shown to customers |
| description |
Create a customer-typable promotion code (e.g. SUMMER20) for an existing Stripe coupon — the thing a "use code X" ad needs, since a coupon alone has no code. couponId (from list_stripe_coupons); optional code (letters, digits and dashes; Stripe generates one if omitted — a code with spaces or symbols is refused, never altered), maxRedemptions, expiresAt (YYYY-MM-DD), firstTimeOnly, minimumAmount + minimumAmountCurrency (major units), customerId. Read back. LIVE key: confirm:true after showing the preview. Use it on a payment link with allowPromotionCodes. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| code | string | customer-typable code, letters, digits and dashes; Stripe generates one if omitted |
| confirm | boolean | true on a live key, after the user approves the preview |
| couponId* | string | coupon id to apply, from list_stripe_coupons |
| metadata | object | key/value metadata stored on the code |
| expiresAt | string | last day the code works, YYYY-MM-DD |
| customerId | string | restrict the code to one customer (cus_…) |
| firstTimeOnly | boolean | true = valid only on a customer's first purchase |
Subscribe a Stripe customer (cus_…) to a price (price_…), optional quantity and trialDays. THIS CHARGES THE CUSTOMER'S SAVED PAYMENT METHOD when there is no trial, so it ALWAYS needs confirm:true (test keys included) after you show the user exactly what will be created. Read back; an INCOMPLETE status means no chargeable card is on file. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to charge the saved card; always required, test keys included |
| priceId* | string | recurring price id, price_... |
| metadata | object | key/value metadata stored on the subscription |
| quantity | number | quantity of the price, e.g. number of seats |
| trialDays | number | free trial length in days before the first charge |
| customerId* | string | customer id, cus_... |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"customerId",
"priceId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to charge the saved card; always required, test keys included"
},
"priceId": {
"type": "string",
"description": "recurring price id, price_..."
},
"metadata": {
"type": "object",
"description": "key/value metadata stored on the subscription",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"quantity": {
"type": "number",
"description": "quantity of the price, e.g. number of seats"
},
"trialDays": {
"type": "number",
"description": "free trial length in days before the first charge"
},
"customerId": {
"type": "string",
"description": "customer id, cus_..."
}
}
}INSTALL A TAG in the container's draft workspace: a Google Ads conversion tag, a GA4 event, or a third-party pixel. IT DOES NOT GO LIVE. Everything written here lands in a WORKSPACE, which is a draft, and it does not run on the website until a human opens Tag Manager and publishes the container. Staging NEVER publishes: making a tag live is a separate, explicitly confirmed call (publish_tag_manager_container), because publishing changes what executes for every visitor on the live site. Always tell the user the change is staged, and that it does nothing until it is published. TWO THINGS YOU MUST GET FROM THE CONTAINER RATHER THAN INVENT. (1) type is a Tag Manager type string and Google publishes no machine-readable list of them, so Hermoso forwards whatever you pass rather than validating against a list that would go stale and start refusing real types. Get the right one by calling list_tag_manager_tags and copying the type off a tag of the same kind that already exists, or read it in Tag Manager. (2) firingTriggerId is required and must name at least one real trigger id from list_tag_manager_tags, because a tag with no trigger is installed and never runs, which is worse than not installing it. Use create_tag_manager_trigger if the container has no suitable trigger. 0 credits. Needs Google Tag Manager connected.
| Argument | Type | Description |
|---|---|---|
| name* | string | what a human will look for in the Tag Manager UI |
| type* | string | the Tag Manager tag type string. Copy it off a tag of the same kind already in the container (list_tag_manager_tags) rather than guessing: Google publishes no machine-readable list of these. |
| notes | string | internal note stored on the tag; not shown on the website |
| paused | boolean |
CREATE A TRIGGER, the rule that decides when a tag fires, in the container's draft workspace. Use it when list_tag_manager_tags shows no trigger matching what a conversion needs, for example a purchase confirmation page or a dataLayer purchase event. A trigger on its own fires nothing: it has to be named in a tag's firingTriggerId, so create the trigger, then create or update the tag that uses it. Like every other write here it lands in a DRAFT and does not reach the website until a human publishes the container. type must be one of Tag Manager's own trigger types, and an unrecognised one is refused by name with the accepted list, for free, before any request. A customEvent trigger with no customEventFilter fires on EVERY dataLayer event in the container rather than the one you meant, so that is refused too. 0 credits. Needs Google Tag Manager connected.
| Argument | Type | Description |
|---|---|---|
| name* | string | trigger name, as it appears in Tag Manager |
| type* | string | one of Tag Manager's own trigger types, e.g. pageview, domReady, windowLoaded, customEvent, formSubmission, linkClick, click, historyChange, scrollDepth, elementVisibility, timer. An unknown one is refused by name with the full list. |
| notes | string | internal note stored on the trigger |
| filter | array | Tag Manager Condition objects, each { type, parameter: [...] } |
| workspace | string | older name for gtmWorkspace (MCP only) |
| gtmWorkspace |
Create a NEW TikTok ad account (advertiser) for the user's business, inside a TikTok Business Center that their own TikTok login administers. This is how someone with no TikTok ad account gets one without leaving Hermoso: when the user wants TikTok ads and has no ad account, offer to create one with this tool, and also when they ask for another one. It runs on the user's OWN TikTok Ads connection and in THEIR company name, never Hermoso's. TWO STEPS, ALWAYS. First call it WITHOUT confirm: that creates nothing and either lists what is still missing (with the industry list when industry is missing) or returns the exact account TikTok would create. Ask the user for every missing field and never guess one: account name, legal company name, industry (a tier-2 id from the list), registeredArea (country of registration, e.g. US) and timezone (e.g. America/New_York). Currency must match the Business Center's and defaults to it. Show the user the summary, then call again with confirm:true. A TikTok ad account CANNOT BE DELETED, only disabled in Business Center. It spends nothing: it has no payment method and no campaigns until the user adds billing in TikTok. Accounts registered in mainland China, Hong Kong, France, Brazil or Mexico need uploaded business documents and are refused unless qualificationId reuses a verified one. If the login has no Business Center yet, create_tiktok_ads_business_center makes one. The new account is read back from TikTok and, when the connection can already reach it, ticked on this brand so every TikTok tool can use it. Free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from list_tiktok_ads_business_centers; omit when the login administers exactly one |
| name | string | ad account name, up to 100 characters |
| share | boolean | default true: tick the new account on this brand's TikTok Ads connection |
Create the ad itself — the creative that runs under an existing TikTok ad group. CREATED PAUSED with no override (TikTok’s own default is ENABLED); it spends nothing until set_tiktok_ads_status(confirm:true), and TikTok additionally reviews it before it can ever show. WHERE THE CREATIVE COMES FROM: run upload_tiktok_ads_creative on a finished Hermoso render (or any public https video) and pass back the videoId AND the coverImageId it returns as imageIds — a TikTok video ad needs BOTH, and TikTok rejects any cover whose dimensions differ from the video, so the video’s own cover is the only one that reliably fits. A videoId with no imageIds is refused here, for free. AN IDENTITY IS MANDATORY AND HAS NO DEFAULT: identityId AND identityType both come from list_tiktok_ads_identities and the ad appears publicly as that TikTok account, so let the USER pick and never guess — the call is refused outright without either. SPARK ADS — RUN A REAL ORGANIC POST INSTEAD: pass tiktokItemId (from list_tiktok_ads_identity_posts or list_tiktok_ads_spark_posts) INSTEAD OF videoId, and the ad IS that TikTok post, keeping its own comments, likes and shares under the account that made it. Spark needs an identityType of TT_USER, BC_AUTH_TT or AUTH_CODE — never CUSTOMIZED_USER, which is a Custom Identity and cannot carry one. THIS MATTERS BEYOND STYLE: TikTok is phasing Custom Identity out — ad accounts created on or after January 15, 2026 cannot create non-Spark ads at all, and existing accounts can no longer create them either, for any ad group delivering to Automatic or Select Placement with TikTok included (only Pangle / Global App Bundle campaigns are unaffected). Defaults: adFormat SINGLE_VIDEO, callToAction LEARN_MORE. THE STATUS IS READ BACK FROM TIKTOK’S OWN STORED ROW, never assumed: if it comes back anything other than DISABLE the note is a Warning: warning that the ad would serve the moment its campaign is enabled — print that verbatim and act on it before touching anything above it.
| Argument | Type | Description |
|---|---|---|
| name* | string | the ad name in TikTok Ads Manager — not the copy people see |
Create an ad group under an existing TikTok campaign — the tier that holds the budget, the bid, the placements, the schedule and ALL the targeting. CREATED PAUSED with no override (TikTok would otherwise create it ENABLED, and a live ad group under a paused campaign is exactly how spend escapes a one-level pause); it spends nothing until set_tiktok_ads_status(confirm:true). TIKTOK REQUIRES THIRTEEN FIELDS TOGETHER and its error names only the FIRST missing one per round trip, so the server checks the whole set locally and refuses with EVERY absentee at once — which matters here because QPS is 1 and each round trip costs a second. Sensible defaults are supplied for placement, pacing, promotion type, schedule type, optimization goal, billing event, bid type and budget mode, so the ONE thing you must resolve yourself is LOCATION: locationIds is effectively mandatory (TikTok: "‘location_ids’ or ‘zipcode_ids’ must be specified") and comes from search_tiktok_ads_targeting(kind:"location"). Resolve interest and language ids the same way — a guessed id either fails the create or targets the wrong people, silently. CONVERSION CAMPAIGNS (WEB_CONVERSIONS / CONVERSIONS / PRODUCT_SALES / LEAD_GENERATION) NEED A PIXEL: set optimizationGoal CONVERT, pass pixelId AND optimizationEvent, and billingEvent OCPM — TikTok pairs a billing event with every goal MANUALLY and derives nothing, so the server supplies the right one if you leave it out. Without a pixel TikTok refuses with "Please select a pixel." The result is READ BACK from TikTok, and the pixel and event it reports are the STORED ones.
| Argument | Type | Description |
|---|---|---|
| bid | number | bid price in the advertiser’s own currency — required by the manual bid types |
| name* | string | ad group name in TikTok Ads Manager |
| budget |
Create a TikTok custom audience from what people actually did — the thing that turns TikTok targeting from interests-and-geo into retargeting. audienceType chooses the source and EACH ONE READS A DIFFERENT ID SPACE for eventSourceIds, which is the mistake to avoid: PIXEL takes pixel ids (list_tiktok_ads_pixels), APP takes app ids (list_tiktok_ads_apps), ENGAGEMENT takes AD GROUP ids, ENGAGEMENT_ORGANIC_VIDEO takes TikTok post ids (list_tiktok_ads_identity_posts, max 10), TIKTOK_SHOP takes shop ids, OFFLINE takes offline event-set ids — and a LEAD_GENERATION audience must carry NO eventSourceIds at all, because TikTok errors if the field is present. The `event` on each rule must belong to that audience type: an event from a different family is refused here by name rather than silently building an audience that measures the wrong thing. THE AUDIENCE IS NOT USABLE IMMEDIATELY — TikTok takes up to 48 hours to process it, and until then it cannot be attached to an ad group. Creating one spends nothing: an audience is a definition. The reply is READ BACK from TikTok, because TikTok’s create returns an id and nothing else.
| Argument | Type | Description |
|---|---|---|
| name* | string | ≤128 characters |
| rules* | array | inclusion rules, combined with OR. Each is {event, retentionDays, eventSourceIds?, parameterFilters?} — e.g. {event:"COMPLETE PAYMENT", retentionDays:30, eventSourceIds:["<pixelId>"]} |
| identityId | string | REQUIRED for ENGAGEMENT_ORGANIC_VIDEO and ENGAGEMENT_LIVE_VIDEO — from list_tiktok_ads_identities |
| autoRefresh | boolean | default true at TikTok — keeps the audience updating with new matching people |
Create a TikTok Business Center for the user's business, on their OWN TikTok Ads connection and in THEIR company name, never Hermoso's. Use it when create_tiktok_ads_account reports the login has no Business Center it administers. TWO STEPS, ALWAYS: call it first WITHOUT confirm. That sends nothing and either lists what is missing or returns the exact Business Center and the TikTok terms the user would be agreeing to. Ask the user for every field and never guess one: bcName, companyName (their legal name), registeredArea, timezone (permanent) and currency. Show the user that reply word for word, and call again with confirm:true only after THEY have agreed. The reply is read back from TikTok. Then create the ad account inside it. Free.
| Argument | Type | Description |
|---|---|---|
| type | string | default SELF_SERVICE, the user's own business; SELF_SERVICE_AGENCY for an agency |
| bcName | string | a name for the Business Center; duplicates are refused |
| confirm | boolean | REQUIRED true to create, only after the user has agreed to the terms in the first reply |
| currency | string | three-letter currency, e.g. USD; ad accounts inside must use it |
| timezone | string | IANA time zone, e.g. America/New_York; cannot be changed later |
| companyName | string | the legal name of the user's business |
| contactName |
Create the top tier of a TikTok ad — the campaign, which fixes the OBJECTIVE everything under it optimises toward. CREATED PAUSED AND THERE IS NO OVERRIDE, and TikTok is the one platform where that is NOT the vendor’s own behaviour: TikTok creates ENABLED by default, so Hermoso forces operation_status DISABLE and overrides a caller who asks for ENABLE. It spends nothing until set_tiktok_ads_status(confirm:true). OBJECTIVES that create cleanly, each proven with a real create: TRAFFIC (the default), REACH, VIDEO_VIEWS, ENGAGEMENT, WEB_CONVERSIONS, LEAD_GENERATION, APP_INSTALL, PRODUCT_SALES, CONVERSIONS. RF_REACH (Reach & Frequency) also works but ONLY with budgetMode BUDGET_MODE_INFINITE — TikTok’s own words are "The budget type for Reach & Frequency must be unlimited". Five further values sit in TikTok’s schema enum and are REFUSED BY NAME with the reason (CATALOG_SALES is retired, SHOP_PURCHASES needs TikTok Shop on the advertiser, and APP_PROMOTION / RF_ENGAGEMENT / RF_APP_INSTALL are withheld) — the server owns that list, so a refusal names its cause and is TikTok’s restriction rather than a Hermoso fault. budgetMode is BUDGET_MODE_DAY (the default), BUDGET_MODE_TOTAL, BUDGET_MODE_DYNAMIC_DAILY_BUDGET or BUDGET_MODE_INFINITE. A campaign alone can never serve: build an ad group under it, then an ad. The result is READ BACK from TikTok and says so when the read-back could not be run.
| Argument | Type | Description |
|---|---|---|
| name* | string | the campaign name in TikTok Ads Manager |
| budget | number | campaign budget in the advertiser’s own currency — not needed with BUDGET_MODE_INFINITE |
| objective | string | default TRAFFIC. e.g. TRAFFIC, REACH, VIDEO_VIEWS, ENGAGEMENT, WEB_CONVERSIONS, LEAD_GENERATION, APP_INSTALL, PRODUCT_SALES, CONVERSIONS — or RF_REACH with budgetMode BUDGET_MODE_INFINITE. The server validates it and refuses an unavailable one BY NAME with TikTok’s reason. |
Create a product catalog inside a TikTok Business Center — the container Dynamic Product Ads sell from. REGION AND CURRENCY ARE SET ONCE AND CAN NEVER BE CHANGED: TikTok’s own catalog-update endpoint changes the NAME and nothing else, so a catalog created in the wrong region or currency has to be replaced. Confirm both with the user before calling, and use list_tiktok_ads_catalogs(regions:true) to see which regions this Business Center may use. The new catalog IS EMPTY — products arrive through a scheduled feed, so the next step is always manage_tiktok_ads_catalog_feed. ENTERTAINMENT and MINI_SERIES catalogs are allowlist-only at TikTok and are offered rather than refused, because an allowlisted advertiser can use them. Creating a catalog spends nothing.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from list_tiktok_ads_business_centers |
| name* | string | ≤128 characters |
| channel | string | also immutable once set |
| currency* | string | IMMUTABLE. The currency for that region, e.g. USD |
| regionCode* | string | IMMUTABLE. The primary targeting region, e.g. US |
| catalogType | string | default ECOM — what a shop selling physical products wants |
Create a CRM Event set — the container for lead-lifecycle events, and the id send_tiktok_ads_events needs as eventSourceId when eventSource is "crm". Making one is how a LEAD_GENERATION campaign stops optimising toward form fills and starts optimising toward leads that qualify and close. Warning: TIKTOK PUBLISHES CREATE AND LIST FOR THESE AND NOTHING ELSE — no update and no delete anywhere in its API reference — so a set made here is PERMANENT, in the same way a TikTok pixel is, and an advertiser is capped at fifty with no API way to free a slot. Name it something the user will still recognise in a year. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | max 40 characters; TikTok trims it and refuses a duplicate. There is no way to rename or delete it afterwards. |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "max 40 characters; TikTok trims it and refuses a duplicate. There is no way to rename or delete it afterwards."
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Create a TikTok Lookalike Audience — TikTok finds people who resemble a seed audience you already have. The seed must exist, must hold AT LEAST 100 people, and cannot itself be a lookalike. audienceSize trades reach against similarity: NARROW is closest to the seed, BROAD reaches furthest. includeSource has NO DEFAULT and TikTok requires it — it decides whether the seed’s own people remain inside the new audience, which matters because including them double-targets people you are already reaching. LOOKALIKE LOCATIONS ARE A SHORTER LIST THAN ORDINARY AD TARGETING (39 countries), so a location id that works on an ad group is not automatically valid here; an unsupported one is refused by name with the full list rather than sent. Takes up to 48 hours to process, and cannot be attached to a Reach & Frequency ad group. Creating one spends nothing.
| Argument | Type | Description |
|---|---|---|
| name* | string | ≤128 characters |
| mobileOs | string | default ALL |
| placements* | array | one or more of "TikTok", "TopBuzz & BuzzVideo", "Pangle" — spelled exactly as TikTok publishes them |
| locationIds* | array | two-letter codes from TikTok’s LOOKALIKE list, e.g. US, GB, DE, JP |
| advertiserId | string | from list_tiktok_ads_accounts |
| audienceSize* | string |
Create a new TikTok pixel on an ad account so conversion campaigns have something to optimise toward. Returns the pixel id and the pixel CODE plus the script to install. A PIXEL ALONE IS NOT ENOUGH AND THIS IS THE PART PEOPLE MISS: it measures nothing until (1) the script is installed on the website and (2) the specific conversion EVENT is defined on it — until then TikTok refuses a conversion ad group with "This pixel event type does not exist." TikTok caps the name at 40 characters, rejects emojis and rejects DUPLICATE names, and it recommends naming the pixel after the site it measures. Creating a pixel spends nothing and serves nothing — it is a measurement definition. NOTE: TikTok publishes NO endpoint that deletes a pixel, so a pixel created here is permanent on that ad account.
| Argument | Type | Description |
|---|---|---|
| name* | string | ≤40 characters, no emojis, and it must not duplicate an existing pixel name on the account. TikTok recommends the website or domain it measures. |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "≤40 characters, no emojis, and it must not duplicate an existing pixel name on the account. TikTok recommends the website or domain it measures."
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}Create the ad group of a REACH & FREQUENCY campaign. THIS IS THE ONE OBJECT HERMOSO CREATES THAT CANNOT BE BORN PAUSED, and that is TikTok’s doing, not ours: /adgroup/rf/create/ publishes no operation_status anywhere in its parameter table, so there is no field to force — and an R&F ad group is a RESERVATION whose budget locks 5 minutes before delivery. So it needs confirm:true, and WITHOUT confirm NOTHING IS CREATED: you get the sentence describing exactly what would be booked, which you must show the user before asking for a yes. The three reservation numbers must come from tiktok_ads_rf_inventory_estimate — TikTok silently books its own maximum rather than refusing a number outside the estimated range. REACH & FREQUENCY IS ALLOWLIST-ONLY PER AD ACCOUNT: TikTok requires the advertiser to be allowlisted by a TikTok representative and to have signed a Commercial Contract for Branding, no API reports that status, and if TikTok refuses this the remedy is a conversation with the rep rather than a different field.
| Argument | Type | Description |
|---|---|---|
| name* | string | ad group name in TikTok Ads Manager |
| budget* | number | from the estimate |
| gender | string | TikTok gender value; omit to reach everyone |
| confirm | boolean | REQUIRED to book. Without it nothing is created and you get the sentence to show the user. |
| feedType | string | STANDARD_FEED (large inventory) or TOP_FEED (limited inventory, higher CPM) |
AUTOMATED RULES — a standing instruction TikTok runs on the user’s ad account on TIKTOK’S schedule, with nobody watching. WHAT A RULE CAN DO UNATTENDED, depending on its actions: pause a campaign / ad group / ad (TURN_OFF), email a report (MESSAGE), LOWER a daily budget, lifetime budget or bid (DECREASE) — or TURN AN OBJECT BACK ON and RAISE a budget or bid (TURN_ON, INCREASE, ADJUST_TO). The second group is standing permission to spend the user’s money without anyone present, on objects the rule re-selects at every execution, so treat it as such and say so before creating one. EVERY RULE IS CREATED TURNED OFF AND READ BACK TO PROVE IT: TikTok publishes no status field on its create and its own example shows a fresh rule running, so Hermoso creates it and disarms it in a second call — and if that second call fails the reply says LOUDLY that a live rule is on the account and how to switch it off. ARMING IS A SEPARATE ACT (set_tiktok_ads_rule_status), which is what makes the rule inspectable in between. An INCREASE or DECREASE must carry value.limit; TikTok requires it and it IS the safety rail — an increase without a ceiling ratchets upward every time the rule fires. TWO THINGS TIKTOK ITSELF PUBLISHES ABOUT THIS ENDPOINT: it is "supported exclusively for direct advertisers" and says SaaS platforms managing multiple advertisers cannot use it, so it may simply be refused for this account — that is THEIR restriction, not a broken connection and no reconnect changes it; and every notification it generates goes to the developer address registered on the app rather than to the user, so tiktok_ads_rule_results is the only place they will see what a rule did. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | what this rule is called in TikTok Ads Manager — up to 512 characters |
| tzone | string | timezone for the rule's time windows, sent to TikTok as given |
| actions* |
Add the creative to a Smart+ ad group. Smart+ combines the materials you give it rather than running one fixed cut, so this is where the videos and copy go. Created PAUSED. If TikTok accepts the request but returns no ad id, the reply says nothing is confirmed to exist rather than claiming success — check Ads Manager before retrying, or a second call can create a twin.
| Argument | Type | Description |
|---|---|---|
| name | string | ad name; "" lets TikTok name it after the ad id |
| adTexts | array | up to 5 ad texts; required unless every creative is a tiktok_item_id Spark post |
| adGroupId* | string | from create_tiktok_smart_ad_group |
| creatives | array | up to 50 TikTok creative_info objects: {ad_format: "SINGLE_VIDEO" | "CAROUSEL_ADS", video_info: {video_id}, image_info: [{web_uri}], tiktok_item_id, identity_type, identity_id}. Ids come from upload_tiktok_ads_creative and list_tiktok_ads_identities |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| playableUrls | array | up to 5 playableUrl values from upload_tiktok_ads_playable. App Promotion ads with a SINGLE_VIDEO creative only; TikTok allowlists Playable Ads per advertiser |
Add an ad group to a Smart+ campaign — the level holding the targeting, budget, schedule and bidding. TikTok requires NINE fields together here and reports only the FIRST one it hits per request, so every absentee is named at once rather than one per round trip. Created PAUSED; it has no ads until you add one.
| Argument | Type | Description |
|---|---|---|
| bid | number | the Cost Cap target, required with bidType BID_TYPE_CUSTOM. Sent as bid_price for CLICK and conversion_bid_price for CONVERT, TRAFFIC_LANDING_PAGE_VIEW, INSTALL and IN_APP_EVENT; VALUE takes deepBidType and roasBid instead |
| name* | string | ad group name |
| budget | number | ad group budget in the advertiser currency. Required when the campaign has budgetOptimizeOn false, ignored when CBO is on |
| bidType* | string | BID_TYPE_NO_BID (automatic bidding) or BID_TYPE_CUSTOM (cost cap, needs bid) |
| pixelId | string | required for WEB_CONVERSIONS / LEAD_GENERATION when optimizationGoal is CONVERT or VALUE |
| roasBid | number | target ROAS (0.01-1000), required when deepBidType is VO_MIN_ROAS |
| targeting* |
Create an UPGRADED SMART+ campaign — TikTok’s automated campaign type, where TikTok picks the audience, placements and creative combinations for you. It is what a customer means by "smart campaign", "automated campaign", "performance max" or "Smart+" on TikTok. CREATED PAUSED WITH NO OVERRIDE, exactly like the classic lane and for the same reason: TikTok creates ENABLED by default, so an omitted status would ship something that serves on the next auction. It spends nothing until set_tiktok_smart_status(confirm:true). SMART+ TAKES A SHORTER OBJECTIVE LIST THAN A CLASSIC CAMPAIGN — APP_PROMOTION, WEB_CONVERSIONS, LEAD_GENERATION and nothing else — and a value TikTok accepts on the classic endpoint can still be refused here, at the ad group, after the campaign exists; that is refused up front instead. CATALOG ADS: set catalogEnabled and catalogType (GENERIC for itemized NON-commerce inventories, added by TikTok on 2026-09-04; PRODUCT for a commerce catalog).
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name |
| budget | number | campaign budget in the advertiser currency |
| objective* | string | Smart+ supports these three ONLY |
| budgetMode | string | the valid set depends on budgetOptimizeOn: DYNAMIC_DAILY_BUDGET or TOTAL with CBO on (the default), INFINITE / DAY / TOTAL with CBO off. Everything except INFINITE also needs budget |
| catalogType | string |
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.
| Argument | Type | Description |
|---|---|---|
| logoUrl* | string | PERMANENT: JPG/PNG/GIF, 1:1, ≤5MB, public URL |
| brandName* | string | PERMANENT: ≤60 characters |
| brandWebsite* | string | must start with https:// |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
| brandIndustryId* | string | PERMANENT: TikTok’s Brand Profile industry id |
| tiktokAccountUrl | string | https://www.tiktok.com/@handle |
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.
| Argument | Type | Description |
|---|---|---|
| anchorId | string | from manage_tiktok_tto_anchor |
| brandName | string | ≤60 characters, when there is no Brand Profile |
| handleNames | array | required for CAMPAIGN: TikTok usernames without the @, max 100 |
| campaignName | string | required for CAMPAIGN; shown to creators, ≤120 characters: TikTok suggests [product name - product description] |
| campaignType |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | unique per account |
| users | array | one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId} |
| emails | array | plain addresses or 64-hex SHA-256 hashes |
| phones | array | E.164, e.g. +14155550100, or hashes |
| handles | array | X handles, with or without @ |
| accountId | string | older name for adAccountId (MCP only) |
| deviceIds | array | IDFA / AdID / Android id |
| expiresAt | string | ISO 8601; X defaults to 13 months |
Create a campaign on X (Twitter). ALWAYS CREATED PAUSED with no override — it spends NOTHING until set_x_ads_status(confirm:true). A CAMPAIGN ALONE CANNOT SERVE ON X: it needs a line item and a promoted post underneath it, and the read-back says so rather than letting you call it a finished ad. Requires a funding instrument (a payment method on the X ad account) — omit fundingInstrumentId to be shown the usable ones, and if there are none this refuses with that reason instead of failing at X. Budgets are in the ad account’s own currency. Everything is READ BACK from X before you are told it exists; print the returned note verbatim.
| Argument | Type | Description |
|---|---|---|
| name* | string | campaign name |
| endTime | string | ISO 8601 end time; X deprecates campaign schedules, so set the schedule on the line item |
| accountId | string | older name for adAccountId (MCP only) |
| startTime | string | ISO 8601 |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| dailyBudget | number | in the ad account’s currency |
| totalBudget | number | lifetime budget in ad account currency |
Create an X card — the big image/video with a headline or button that an X ad clicks through on. ONE destination: websiteUrl (a website card, with a headline `title`), leadFormId (opens an X lead form — headline, or a button with buttonLabel), or an app (iosAppId/googlePlayAppId + countryCode, a button). Media: mediaKey, or 2–6 mediaKeys for a swipeable card (upload_x_ads_media). utm {source,medium,campaign,content,term} is appended to websiteUrl without overwriting the URL's own. Read back from X; a card is used by create_x_ads_post(cardUri). Free.
| Argument | Type | Description |
|---|---|---|
| utm | object | UTM tags object: source, medium, campaign, content, term; appended to websiteUrl |
| name* | string | internal name for the card |
| title | string | headline, for website and lead-form cards |
| iosAppId | string | iOS App Store app id, for an app card |
| mediaKey | string | one media key from upload_x_ads_media (single image or video) |
| accountId | string | older name for adAccountId (MCP only) |
| mediaKeys | array | 2-6 media keys from upload_x_ads_media, for a swipeable card |
| leadFormId | string |
Create an X lead form — the native form an X lead ad opens, so people sign up without leaving X. The X lead ad chain: lead form -> create_x_ads_card(leadFormId) -> create_x_ads_post(cardUri, lineItemId) on a WEBSITE_CLICKS line item (X retired the LEAD_GENERATION objective) -> get_x_ads_leads. Needs name, purpose, fields (at least one required), and an https privacy policy URL + company name. X ties every form to a conversion event: an ad account with no X Pixel needs conversionEventId, and the refusal says so plainly. X also builds a customer-list audience of the form's leads. Read back from X. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | internal name of the lead form |
| fields* | array | at least one must not be optional |
| purpose* | string | what the leads are used for (shown to the person) |
| headline | string | intro card headline |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| companyName* | string | company name shown on the form |
Create a line item — X's ad group — under a campaign. ALWAYS CREATED PAUSED with no override. THE TREE ON X IS campaign -> line item -> promoted post, and a campaign ALONE CANNOT SERVE: this is the middle level, and it still cannot serve until you attach a post with create_x_ads_promoted_tweet. Targeting attaches HERE (add_x_ads_targeting), never to the campaign. `objective` is validated before dispatch because X answers an invalid one with a 500 that reads like an outage; note that WEBSITE_CONVERSIONS and SITE_VISITS are `goal` values and are NOT objectives. `bidStrategy` MAX/TARGET require a bidAmount; AUTO lets X set it. Omitting startTime records now, and the read-back says so. Everything is READ BACK from X before you are told it exists; print the returned note verbatim.
| Argument | Type | Description |
|---|---|---|
| goal | string | optimization goal, e.g. LINK_CLICKS or WEBSITE_CONVERSIONS; these are goals, not objectives |
| name | string | line item (ad group) name |
| endTime | string | ISO 8601 end time for this line item |
| accountId | string | older name for adAccountId (MCP only) |
| bidAmount | number | required for MAX/TARGET; in the ad account's currency |
| objective* | string | one per campaign (X refuses a second objective). For an X LEAD AD use WEBSITE_CLICKS with a lead-form card — X retired LEAD_GENERATION |
Write an X post FOR ADS — PROMOTED-ONLY by default (X's 'nullcast'): it never appears on the brand's timeline or in followers' feeds and serves only as an ad, so you can test several creatives without spamming followers. Carry a card (cardUri from create_x_ads_card) OR up to 4 media (mediaKeys from upload_x_ads_media). Posted as the ad account's own promotable user (asUserId to choose). Pass lineItemId to attach it straight to a line item (which stays PAUSED). promotedOnly:false makes it a normal public post and needs confirmPublic:true. Read back from X. Free.
| Argument | Type | Description |
|---|---|---|
| utm | object | UTM tags object: source, medium, campaign, content, term; appended to the destination URL |
| name | string | internal name in ads.x.com |
| text | string | post text |
| cardUri | string | card_uri from create_x_ads_card; use instead of mediaKeys |
| asUserId | string | promotable user id to post as; omit for the ad account's own user |
| videoCta | string | call-to-action button on a video post: VISIT_SITE or WATCH_NOW |
| accountId | string | older name for adAccountId (MCP only) |
| mediaKeys | array | up to 4 media keys from upload_x_ads_media |
THE CREATIVE OF AN X FOLLOWER CAMPAIGN. A line item with objective FOLLOWERS (productType PROMOTED_ACCOUNT, which create_x_ads_line_item now defaults to for FOLLOWERS) promotes an X ACCOUNT, and until one is attached here it has nothing to show and never serves. Omit userId and the ad account’s own promotable account is used; if it may promote several, the choice is refused rather than guessed (list_x_ads_promotable_users shows them). A line item of any other product type is refused for free, naming X’s INCOMPATIBLE_LINE_ITEM rule. You may ALSO attach a post with create_x_ads_promoted_tweet, which X adds as a mobile timeline placement. The attachment is READ BACK from the line item before it is reported. Nothing spends: the campaign and line item stay PAUSED until set_x_ads_status(confirm:true). X cannot edit a promoted account, only remove it (delete_x_ads_object type:"promoted_account"). Free.
| Argument | Type | Description |
|---|---|---|
| userId | string | the numeric X user id to promote — omit for the ad account’s own promotable account |
| accountId | string | older name for adAccountId (MCP only) |
| lineItemId* | string | a PROMOTED_ACCOUNT line item (objective FOLLOWERS) from create_x_ads_line_item |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"lineItemId"
],
"properties": {
"userId": {
"type": "string",
"description": "the numeric X user id to promote — omit for the ad account’s own promotable account"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"lineItemId": {
"type": "string",
"description": "a PROMOTED_ACCOUNT line item (objective FOLLOWERS) from create_x_ads_line_item"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}Attach an existing X post to a line item so the line item has a creative — the LAST piece of the tree, and without it nothing can ever serve however the statuses are set. The tweet id is the number at the end of the post URL (x.com/<handle>/status/<id>), not the URL. Several ids may be attached at once; X creates one promoted-tweet row per post. ATTACHMENT IS VERIFIED by re-reading the line item's own promoted posts rather than by trusting X's echo, and if that read cannot run the note says UNCONFIRMED instead of claiming success. Everything Hermoso built above this is PAUSED, so attaching a post starts no spend. Print the returned note verbatim.
| Argument | Type | Description |
|---|---|---|
| tweetIds* | array | post ids — digits only, from the end of the post URL |
| accountId | string | older name for adAccountId (MCP only) |
| lineItemId* | string | from create_x_ads_line_item |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"lineItemId",
"tweetIds"
],
"properties": {
"tweetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "post ids — digits only, from the end of the post URL"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"lineItemId": {
"type": "string",
"description": "from create_x_ads_line_item"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually delete. Without it nothing changes and the key event is described back to you |
| property* | string | the NUMERIC GA4 property id from list_analytics_properties |
| eventName* | string | the key event to stop counting, e.g. "sign_up" — from list_analytics_definitions |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"eventName"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually delete. Without it nothing changes and the key event is described back to you"
},
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties"
},
"eventName": {
"type": "string",
"description": "the key event to stop counting, e.g. \"sign_up\" — from list_analytics_definitions"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| id* | string | REQUIRED. |
| level* | string | REQUIRED. |
PERMANENTLY delete a profile and EVERYTHING in it — brand details, memory, swipefile, Library creations, generated assets, avatars, skills, playbooks, chats — and disconnect its connected accounts. Irreversible, and it applies to everyone the workspace is shared with. Call it WITHOUT confirm first: it reports exactly what that workspace holds. Show the user that inventory verbatim, get an unambiguous yes, then call again with confirm:true — plus, if the workspace is not empty, confirmName set to its exact name and confirmConnectors set to the number of connected accounts it reported. Those two exist because confirming INTENT does not prove you picked the right WORKSPACE, and a wrong target is how a live profile was destroyed. The account's FIRST/anchor profile cannot be deleted this way (it holds the workspace's root storage) — that one is replaced from the app.
| Argument | Type | Description |
|---|---|---|
| brand* | string | profile id or exact name from list_brands |
| confirm | boolean | REQUIRED true — this destroys the whole profile and cannot be undone |
| confirmName | string | the workspace's EXACT name, required when it is not empty — copy it from the inventory this tool returned, after the user has agreed to it |
| confirmConnectors | number | the number of connected accounts the inventory reported, required when there is at least one — the user must specifically agree to losing them, because reconnecting each needs a browser and no agent can do it |
Remove a saved creator from this workspace’s cast by id (from list_creators). Records a cross-device delete so they don’t reappear on the user’s other devices. It only drops the roster entry — ads already rendered with that person are untouched — and the same portrait can be saved again with save_creator, so no confirm is needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the creator id (from list_creators) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the creator id (from list_creators)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Delete a Drive file. By default it goes to Trash (recoverable); pass permanent:true to delete it forever. Pass fileId (from list_drive_files) + confirm:true. Irreversible when permanent — confirm with the user first.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the Drive file id |
| confirm | boolean | REQUIRED true |
| permanent | boolean | true = delete forever; default trashes (recoverable) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the Drive file id"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
},
"permanent": {
"type": "boolean",
"description": "true = delete forever; default trashes (recoverable)"
}
}
}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").
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id (level:"ad" — pass adGroupId too) |
| level | string | what to remove — default campaign |
| confirm | boolean | REQUIRED true — REMOVED is permanent |
| adGroupId | string | ad group id (level:"adGroup"; REQUIRED as the parent for "ad" and "keyword") |
PERMANENTLY delete images/videos from this brand's Library: the entry and the stored file. Pass ids (L0… from list_library) or urls. Without confirm it lists what would be deleted; ask the user, then call again with confirm:true. Free.
| Argument | Type | Description |
|---|---|---|
| ids | array | Library ids from list_library |
| urls | array | file links |
| confirm | boolean | true deletes |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "Library ids from list_library"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "file links"
},
"confirm": {
"type": "boolean",
"description": "true deletes"
}
}
}Delete a LinkedIn campaign group, campaign or creative (level:"creative" + creativeId). LinkedIn HARD-deletes only DRAFT objects; anything that has ever run is moved to PENDING_DELETION instead — it stops serving and its reporting history is retained. The returned note says which of the two actually happened, and you must repeat that rather than claiming a clean delete. Irreversible either way, so it requires confirm:true.
| Argument | Type | Description |
|---|---|---|
| level | string | default campaign |
| confirm* | boolean | REQUIRED true — this is irreversible |
| campaignId | string | campaign id to delete when level is campaign |
| creativeId | string | creative (ad) id to delete when level is creative |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
| campaignGroupId | string | campaign group id to delete when level is campaignGroup |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"confirm"
],
"properties": {
"level": {
"enum": [
"campaign",
"campaignGroup",
"creative"
],
"type": "string",
"description": "default campaign"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — this is irreversible"
},
"campaignId": {
"type": "string",
"description": "campaign id to delete when level is campaign"
},
"creativeId": {
"type": "string",
"description": "creative (ad) id to delete when level is creative"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
},
"campaignGroupId": {
"type": "string",
"description": "campaign group id to delete when level is campaignGroup"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually delete; without it nothing is deleted and the product count that would go with it is reported |
| dataSource* | string | the feed to delete, from list_merchant_data_sources |
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"dataSource"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually delete; without it nothing is deleted and the product count that would go with it is reported"
},
"dataSource": {
"type": "string",
"description": "the feed to delete, from list_merchant_data_sources"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually delete; without it nothing is deleted and the real product is reported back |
| offerId* | string | your offer id of the product to delete |
| feedLabel | string | market label of the product, e.g. US; taken from the feed when it declares one |
| 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 |
| merchantCenterId* | string | from list_merchant_accounts |
PERMANENTLY delete a Meta custom audience or lookalike. Meta’s own warning: "When you delete a custom audience, it will be permanently removed from your account and your ads using it will stop running." An audience is the one ad object whose value is its CONTENTS — a big retargeting list cannot be rebuilt, it has to re-accumulate — so CALL IT WITHOUT confirm FIRST: nothing is deleted, and you get its real name, how many people are in it and which lookalikes were built from it, read live from Meta. Show the user exactly that. A populated audience, or one with lookalikes, then needs confirmName set to its exact name (and confirmChildren set to the lookalike count when there are any). META REFUSES to delete an audience that has lookalikes derived from it (error 2656) — delete those first; the unconfirmed call names them. Pass adAccountId + audienceId (from list_meta_audiences). The result is READ BACK from Meta: it says deleted only when the id no longer resolves.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true — the deletion is permanent |
| audienceId* | string | the custom audience id (from list_meta_audiences) |
| adAccountId* | string | ad account id (act_… or digits) |
| confirmName | string | the audience’s EXACT name, required when it holds people or has lookalikes |
| confirmChildren | number | the exact number of derived lookalikes reported by the unconfirmed call, required when it has any |
PERMANENTLY delete a campaign, ad set, or ad. Pass objectId (from list_meta_ads) + adAccountId. DELETING A CAMPAIGN ALSO DELETES EVERY AD SET AND AD UNDER IT, and deleting an ad set deletes its ads — one id, the whole tree. Call it WITHOUT confirm first: it reports what the object is, its name, and how many children go with it. Show the user that, get an unambiguous yes, then call again with confirm:true plus confirmChildren set to the number it reported (only needed when there is at least one child). To stop delivery without deleting anything, use update_meta_object(status:"PAUSED") instead — that is reversible and this is not.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true — deletion is permanent |
| objectId* | string | the campaign / ad set / ad id to delete |
| adAccountId* | string | ad account id (for auth + scope) |
| confirmChildren | number | the number of child ad sets + ads this delete also destroys, as reported by the unconfirmed call — required whenever that is above zero |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"objectId",
"adAccountId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true — deletion is permanent"
},
"objectId": {
"type": "string",
"description": "the campaign / ad set / ad id to delete"
},
"adAccountId": {
"type": "string",
"description": "ad account id (for auth + scope)"
},
"confirmChildren": {
"type": "number",
"description": "the number of child ad sets + ads this delete also destroys, as reported by the unconfirmed call — required whenever that is above zero"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id (level:"ad" — pass adGroupId too) |
| level | string | what to delete — default campaign |
| confirm | boolean | REQUIRED true — the delete is permanent |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId |
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true — call without it first to see Microsoft’s own advice about the 12-hour delivery tail |
| accountId | string | older name for adAccountId (MCP only) |
| productId* | string | the FULLY QUALIFIED id (channel:contentLanguage:targetCountry:offerId), not the offerId |
| merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"productId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true — call without it first to see Microsoft’s own advice about the 12-hour delivery tail"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"productId": {
"type": "string",
"description": "the FULLY QUALIFIED id (channel:contentLanguage:targetCountry:offerId), not the offerId"
},
"merchantId": {
"type": "string",
"description": "Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true only after the user approved deleting this exact annotation |
| annotationId* | number | from list_mixpanel_annotations |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"annotationId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true only after the user approved deleting this exact annotation"
},
"annotationId": {
"type": "number",
"description": "from list_mixpanel_annotations"
}
}
}Delete a OneDrive item — it moves to the OneDrive recycle bin (recoverable there). Pass fileId (from list_onedrive_files) + confirm:true. Confirm the exact file with the user first.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the OneDrive item id |
| confirm | boolean | REQUIRED true |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the OneDrive item id"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
}
}
}Delete the lead-sync subscription: every lead form on the account stops delivering leads. confirm:true required.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to delete; every lead form on the account stops delivering leads |
| subscriptionId* | string | lead-sync subscription id, from the subscriptions list of list_openai_ads_leads |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"subscriptionId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to delete; every lead form on the account stops delivering leads"
},
"subscriptionId": {
"type": "string",
"description": "lead-sync subscription id, from the subscriptions list of list_openai_ads_leads"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id to archive; use with level ad |
| level | string | what to archive — default campaign |
| confirm | boolean | REQUIRED true — archiving cannot be undone |
| adGroupId | string | ad group id to archive; use with level adGroup |
| campaignId | string | campaign id to archive; use with level campaign |
Delete an active or scheduled spend-limit window before it ends — this REMOVES a spending ceiling. confirm:true required.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to delete the window |
| windowId* | string | spend-limit window id to delete; removes that spending ceiling |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"windowId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to delete the window"
},
"windowId": {
"type": "string",
"description": "spend-limit window id to delete; removes that spending ceiling"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id to archive; used with level:"ad" |
| level | string | what to archive — default campaign |
| confirm | boolean | REQUIRED true — archiving is not self-service reversible |
| adGroupId | string | ad group id to archive; used with level:"adGroup" |
| campaignId | string | campaign id to archive; used with level:"campaign" |
Delete a saved playbook by id (from list_playbooks). Records a cross-device delete so it does not come back on the next sync. Minor + re-creatable, so no confirm needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the playbook id (from list_playbooks) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the playbook id (from list_playbooks)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| assetId* | string | id of the asset to delete |
| confirm | boolean | true required; without it nothing is deleted |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"assetId"
],
"properties": {
"assetId": {
"type": "string",
"description": "id of the asset to delete"
},
"confirm": {
"type": "boolean",
"description": "true required; without it nothing is deleted"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true — the deletion is permanent |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| customAudienceId* | string | id of the custom audience to delete, from list_reddit_ads_audiences |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"customAudienceId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true — the deletion is permanent"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"customAudienceId": {
"type": "string",
"description": "id of the custom audience to delete, from list_reddit_ads_audiences"
}
}
}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).
| Argument | Type | Description |
|---|---|---|
| id* | string | id of the catalog, feed or product set to delete |
| kind* | string | catalog, feed or product_set: which type the id refers to |
| confirm | boolean | true only after the user approved the listed impact |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"kind",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "id of the catalog, feed or product set to delete"
},
"kind": {
"enum": [
"catalog",
"feed",
"product_set"
],
"type": "string",
"description": "catalog, feed or product_set: which type the id refers to"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved the listed impact"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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").
| Argument | Type | Description |
|---|---|---|
| id* | string | id of the campaign, ad group or ad to remove |
| kind* | string | campaign, ad_group or ad: which type the id refers to |
| status | string | DELETED = permanent (default); ARCHIVED = out of service but reversible to PAUSED |
| confirm | boolean | REQUIRED true |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true only after the user approved the listed impact |
| catalogId* | string | catalog the products belong to |
| productIds* | array | product ids (your SKUs) to remove, up to 1,000 per call |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId",
"productIds"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true only after the user approved the listed impact"
},
"catalogId": {
"type": "string",
"description": "catalog the products belong to"
},
"productIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "product ids (your SKUs) to remove, up to 1,000 per call"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| 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 |
| savedAudienceId* | string | id of the saved audience to delete |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"savedAudienceId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"confirmName": {
"type": "string",
"description": "the audience’s EXACT name, required when live ad groups use it"
},
"confirmChildren": {
"type": "number",
"description": "the exact number of live ad groups reported by the unconfirmed call"
},
"savedAudienceId": {
"type": "string",
"description": "id of the saved audience to delete"
}
}
}Remove a property from the connected Google account. THIS LOSES HISTORY: the account loses access to that property's stored Search Console performance data, and re-adding it later means verifying ownership from scratch — Google does NOT restore the 16-month window on re-add. Nothing about the site itself, its pages or its ranking changes. CONFIRM-GATED: the unconfirmed call removes nothing and describes what would be lost, including how many sitemaps this account can currently see on it. The property is also dropped from this brand's shared list, so it cannot linger as a tick pointing at something unreachable. 0 credits.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to actually remove it |
| siteUrl* | string | the exact property string from list_search_console_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to actually remove it"
},
"siteUrl": {
"type": "string",
"description": "the exact property string from list_search_console_sites"
}
}
}Remove a sitemap from a property. THIS DOES NOT REMOVE THOSE PAGES FROM GOOGLE'S INDEX — it stops Google using that file to discover new ones, and it can be submitted again at any time, so it is reversible. CONFIRM-GATED: called without confirm it removes nothing and reports what the sitemap actually IS, read back from Google (its error and warning counts and how many URLs it covers), so you can check you are aiming at the right one. Re-run with confirm:true to proceed. 0 credits.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to actually remove it. Without this nothing is removed and the sitemap is described back to you. |
| siteUrl* | string | the exact property string from list_search_console_sites |
| feedpath* | string | the FULL sitemap URL to remove |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"feedpath"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to actually remove it. Without this nothing is removed and the sitemap is described back to you."
},
"siteUrl": {
"type": "string",
"description": "the exact property string from list_search_console_sites"
},
"feedpath": {
"type": "string",
"description": "the FULL sitemap URL to remove"
}
}
}Remove a marketing activity Hermoso recorded from the merchant's Shopify > Marketing, by remoteId or id. Shopify stops attributing visits and sales to it and it cannot be undone, so without confirm:true NOTHING is deleted and the reply names exactly what would go: show the user, get a yes, then call again with confirm:true. Read back afterwards. Only works for accounts created by installing Hermoso from the Shopify App Store. Free.
| Argument | Type | Description |
|---|---|---|
| id | string | gid://shopify/MarketingActivity/… |
| confirm | boolean | true to delete; without it the reply names what would be removed and deletes nothing |
| remoteId | string | remoteId of the activity to delete; use this or id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "gid://shopify/MarketingActivity/…"
},
"confirm": {
"type": "boolean",
"description": "true to delete; without it the reply names what would be removed and deletes nothing"
},
"remoteId": {
"type": "string",
"description": "remoteId of the activity to delete; use this or id"
}
}
}Delete one of the workspace’s CUSTOM skills by id (from list_skills). Built-in skills/recipes can’t be deleted. Minor + re-creatable, so no confirm needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the custom skill id (from list_skills) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the custom skill id (from list_skills)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Permanently delete a Snapchat audience — Snapchat: deleted audiences "cannot be retrieved". confirm:true required; without it nothing is deleted and you get the audience's name, type and size to show the user. Ad squads targeting it lose that targeting. Read back. Free.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true |
| audienceId* | string | audience id to delete, from list_snapchat_ads_audiences |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
},
"audienceId": {
"type": "string",
"description": "audience id to delete, from list_snapchat_ads_audiences"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}PERMANENTLY DELETE a Snapchat campaign, ad squad or ad. Snapchat deletes at every tier, so this is irreversible and there is no undelete. Needs confirm:true, and the unconfirmed call changes nothing and names the objects READ FROM SNAPCHAT plus what deleting a parent takes with it (a campaign takes its ad squads and ads). TO STOP DELIVERY REVERSIBLY, use set_snapchat_ads_status with PAUSED instead — the refusal says so, because most people asking to "remove" an ad mean "stop it". The read-back is INVERTED for a delete: an id that still resolves afterwards is reported as NOT CONFIRMED, never as a success.
| Argument | Type | Description |
|---|---|---|
| ids* | array | the objects to delete |
| level* | string | which tier these ids belong to |
| confirm | boolean | REQUIRED true — without it nothing is deleted and you get the blast radius to show the user |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"ids"
],
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "the objects to delete"
},
"level": {
"enum": [
"campaign",
"adsquad",
"ad"
],
"type": "string",
"description": "which tier these ids belong to"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — without it nothing is deleted and you get the blast radius to show the user"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}Delete one or more TikTok custom audiences. IRREVERSIBLE: TikTok publishes no undelete, any Lookalike seeded from one loses its seed, every ad group targeting it stops using it, and re-creating one means rebuilding the rule and waiting up to 48 hours for it to process again. WITHOUT confirm NOTHING IS DELETED — you get each audience’s real name, type and matched-user count READ BACK FROM TIKTOK, which is what you show the user before asking for a yes, because a confirm flag proves you meant to delete something and cannot tell an empty scratch audience from the one every campaign targets. Deleting MORE THAN ONE additionally needs confirmCount set to the exact number. If the audiences cannot be READ, the delete is refused outright rather than performed blind — an unknown blast radius on an irreversible action is not the same as an empty one.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true — without it nothing is deleted and you get the sentence describing what would be |
| audienceIds* | array | up to 100 |
| advertiserId | string | from list_tiktok_ads_accounts |
| confirmCount | number | REQUIRED, and equal to audienceIds.length, when deleting more than one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceIds"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true — without it nothing is deleted and you get the sentence describing what would be"
},
"audienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 100"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"confirmCount": {
"type": "number",
"description": "REQUIRED, and equal to audienceIds.length, when deleting more than one"
}
}
}Warning: READ THIS BEFORE REACHING FOR IT: TikTok’s comment delete removes a comment YOUR OWN advertiser identity posted — your own reply. It CANNOT remove a comment left by a member of the public, and TikTok reports that per comment as canDelete on every row from list_tiktok_ads_comments. To take somebody else’s comment out of public view, use moderate_tiktok_ads_comment with operation HIDDEN, which is the moderation tool and is reversible. IRREVERSIBLE: TikTok publishes no undelete, the replies under it go with it, and re-posting means a new comment with a new id at the bottom of the thread rather than the one people answered. WITHOUT confirm NOTHING IS DELETED — you get the comment’s real text, author and date READ BACK FROM TIKTOK, which is what you show the user before asking for a yes. Deleting then additionally needs confirmText set to that exact text, because a confirm flag proves you meant to delete something and cannot prove you aimed at the right comment. If the comment cannot be READ, the delete is refused outright rather than performed blind.
| Argument | Type | Description |
|---|---|---|
| adId* | string | from the same comment row |
| confirm | boolean | REQUIRED true — without it nothing is deleted and you get the sentence describing what would be |
| commentId* | string | from list_tiktok_ads_comments — one whose canDelete is true |
| identityId* | string | from the same comment row |
| commentType |
Remove TikTok campaigns, ad groups or ads. TIKTOK HAS NO DELETE VERB — removal is modelled as a STATUS, exactly like Reddit — so this posts to the same route as set_tiktok_ads_status with status:"DELETE" rather than being a second implementation with its own rules. It is permanent, TikTok publishes no undelete, and it needs confirm:true; calling it WITHOUT confirm changes nothing and returns the sentence naming how many objects go, which is what you show the user first. TikTok does NOT document whether removing a campaign takes its ad groups and ads with it — each carries its own status — so remove the children yourself if they should go too, and VERIFY with list_tiktok_ads_campaigns afterwards rather than assuming a cascade either way. TO STOP DELIVERY WITHOUT DESTROYING ANYTHING, use set_tiktok_ads_status(status:"DISABLE"): that is reversible and this is not.
| Argument | Type | Description |
|---|---|---|
| ids* | array | the campaign / ad group / ad ids to remove |
| level* | string | which tier the ids belong to |
| confirm | boolean | REQUIRED true — the removal is permanent |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"ids"
],
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "the campaign / ad group / ad ids to remove"
},
"level": {
"enum": [
"campaign",
"adgroup",
"ad"
],
"type": "string",
"description": "which tier the ids belong to"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — the removal is permanent"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Delete a SAVED playable from a TikTok advertiser's Creative Library. Permanent. Without confirm:true it changes nothing and returns what would be deleted, which you show the user first. TikTok does not report which ads use it.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to delete |
| playableId* | string | from list_tiktok_ads_playables |
| confirmName | string | optional name check for the playable being deleted; confirm:true is the gate |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"playableId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to delete"
},
"playableId": {
"type": "string",
"description": "from list_tiktok_ads_playables"
},
"confirmName": {
"type": "string",
"description": "optional name check for the playable being deleted; confirm:true is the gate"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Delete the Spark-Ads authorization code for a post the authorized account owns. CONFIRM-GATED, and not because it spends: ads already built on the post keep running, but no NEW ad can be built against it until a fresh code exists, and TikTok REFUSES this outright while any campaign or ad using the post is still ACTIVE — pause those first. Calling it without confirm changes nothing and returns the sentence describing exactly what would happen. THE READ-BACK INVERTS: proof of success is that TikTok no longer reports a code for the post, and a code that is still there is reported as not confirmed rather than done. NEEDS THE TIKTOK ACCOUNT AUTHORIZATION.
| Argument | Type | Description |
|---|---|---|
| itemId* | string | the TikTok post id |
| confirm | boolean | REQUIRED true — call without it first to see exactly what would change |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"itemId"
],
"properties": {
"itemId": {
"type": "string",
"description": "the TikTok post id"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — call without it first to see exactly what would change"
}
}
}PERMANENTLY delete one Vercel deployment: its URL stops working and Vercel has no undo. Without confirm:true nothing is deleted and you get the deployment it would delete (URL, target, state) to show the user; a production deployment the domains still serve takes the site down. Call again with confirm:true only after the user says yes to that exact deployment. Free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true only after the user approved deleting this exact deployment |
| deploymentId* | string | from list_vercel_deployments |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"deploymentId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true only after the user approved deleting this exact deployment"
},
"deploymentId": {
"type": "string",
"description": "from list_vercel_deployments"
}
}
}PERMANENTLY delete an X campaign, line item, promoted post, promoted account or targeting criterion — or ARCHIVE a lead form (it stops collecting and its leads can no longer be exported, so run get_x_ads_leads first), or remove a card, a Media Library item (type media: id is the media_key) or a custom audience (custom_audience). X CASCADES AND PUBLISHES NO UNDO: deleting a campaign destroys its line items and their promoted posts too — verified live, the children answer 404 the moment the parent is deleted. RUN IT WITHOUT confirm FIRST: that deletes nothing and reports the REAL blast radius read back off X (what is underneath it, whether it is live, whether it has spent). Show the user exactly that, get an unambiguous yes, then call again with confirm:true — and, for anything with children / live delivery / real spend, also confirmName set to its exact name and confirmChildren set to the real count, because confirm:true alone proves you meant to delete SOMETHING and cannot prove you aimed at the right object. TO STOP DELIVERY WITHOUT DESTROYING ANYTHING use set_x_ads_status PAUSED, which is reversible — except for a promoted post, which cannot be paused on X at all, so pause its LINE ITEM instead. Removing a targeting criterion WIDENS the audience rather than narrowing it. The result is confirmed by re-reading the object, never by X's 200.
| Argument | Type | Description |
|---|---|---|
| id* | string | campaign/line-item id, or the id from list_x_ads_promoted_tweets / list_x_ads_targeting |
| type* | string | kind of object the id names; for media, id is the media_key |
| confirm | boolean | omit on the first call to see the blast radius |
Publish a STATIC landing page to the brand's Vercel account from HTML (it becomes index.html) or a few files [{file, data, encoding:'utf-8'|'base64'}], up to 50 files and 4 MB. project names the Vercel project (created if it does not exist; it becomes part of the URL). target 'preview' (default) makes a new preview URL and leaves the live site alone; 'production' REPLACES what the project's domains serve. WITHOUT confirm:true nothing is sent and you get the exact preview to show the user; call again with confirm:true only after they say yes. The answer is Vercel's own deployment row (URL and state); a new deployment is QUEUED or BUILDING for a moment, so read it back with list_vercel_deployments deploymentId until READY. Free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Argument | Type | Description |
|---|---|---|
| html | string | the full HTML of the page |
| files | array | instead of (or as well as) html: relative paths with their content |
| target | string | default preview |
| confirm | boolean | true only after the user approved this exact deploy |
| project* | string | Vercel project name, e.g. spring-sale |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"project"
],
"properties": {
"html": {
"type": "string",
"description": "the full HTML of the page"
},
"files": {
"type": "array",
"items": {
"type": "object",
"required": [
"file",
"data"
],
"properties": {
"data": {
"type": "string",
"description": "file content: text, or base64 text when encoding is base64"
},
"file": {
"type": "string",
"description": "relative path inside the site, e.g. about/index.html; no leading slash and no ../"
},
"encoding": {
"enum": [
"utf-8",
"base64"
],
"type": "string",
"description": "utf-8 (default) for text, or base64 for binary content"
}
}
},
"description": "instead of (or as well as) html: relative paths with their content"
},
"target": {
"enum": [
"preview",
"production"
],
"type": "string",
"description": "default preview"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved this exact deploy"
},
"project": {
"type": "string",
"description": "Vercel project name, e.g. spring-sale"
}
}
}Disconnect a third-party account from this workspace (Meta, Google Ads, Google Drive/Sheets/Docs, YouTube, TikTok, LinkedIn, X, Reddit Ads, Pinterest, Google Business, Microsoft Advertising/OneDrive, Slack, …). This always drops the stored credentials, so every tool for that provider stops working immediately and posts/campaigns already published are NOT affected. IT NEVER REVOKES THE GRANT AT THE PROVIDER: the authorisation stays in place, so the same account keeps working in any other brand it is connected to, until the user removes Hermoso in that provider's own settings. The unconfirmed call names where to do that (list_connectors carries revokesAtProvider:"local") — relay it rather than promising a revoke. RECONNECTING NEEDS A BROWSER (the provider's consent screen) — an agent cannot undo this. Name the provider to the user, then call with confirm:true. Use list_connectors for the exact provider ids.
| Argument | Type | Description |
|---|---|---|
| account | string | on a channel with several connected accounts (TikTok, X, YouTube, Threads, Bluesky, Telegram, Pinterest): remove ONLY this account (@handle or id from list_connector_accounts) and keep the others |
| confirm | boolean | REQUIRED true — reconnecting a sign-in account afterwards needs the user's browser; a paste-a-key account is reconnected with connect_connector |
| provider* | string | provider id exactly as list_connectors reports it, e.g. "meta", "google_ads", "youtube", "linkedin" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"account": {
"type": "string",
"description": "on a channel with several connected accounts (TikTok, X, YouTube, Threads, Bluesky, Telegram, Pinterest): remove ONLY this account (@handle or id from list_connector_accounts) and keep the others"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — reconnecting a sign-in account afterwards needs the user's browser; a paste-a-key account is reconnected with connect_connector"
},
"provider": {
"type": "string",
"description": "provider id exactly as list_connectors reports it, e.g. \"meta\", \"google_ads\", \"youtube\", \"linkedin\""
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| pageSize | number | 1-200, default 24 |
| languages | array | language codes the creator posts in; optional |
| sortField | string | RELEVANCE, FOLLOWERS, MEDIAN_VIEWS or ENGAGEMENT_RATE |
| sortOrder | string | ASC or DESC |
| followerAge | string |
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.
| Argument | Type | Description |
|---|---|---|
| id* | string | REQUIRED — the recommendation id. |
| type | string | Default daily_budget. |
| promotedObjectId* | string | REQUIRED — the same app or brand id the recommendation was queried for. |
| promotedObjectType | string | Default APPSTORE_APP. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id",
"promotedObjectId"
],
"properties": {
"id": {
"type": "string",
"description": "REQUIRED — the recommendation id."
},
"type": {
"enum": [
"daily_budget",
"target_cpa"
],
"type": "string",
"description": "Default daily_budget."
},
"promotedObjectId": {
"type": "string",
"description": "REQUIRED — the same app or brand id the recommendation was queried for."
},
"promotedObjectType": {
"enum": [
"APPSTORE_APP",
"BUSINESS_BRAND"
],
"type": "string",
"description": "Default APPSTORE_APP."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| recommendationIds* | array | ids from microsoft_ads_recommendations, max 100 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"recommendationIds"
],
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"recommendationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ids from microsoft_ads_recommendations, max 100"
}
}
}DOWNLOAD THE ACTUAL LEADS — the names, emails and phone numbers people entered in a TikTok Instant Form. REGION IS REQUIRED AND THERE IS NO "ALL": TikTok keeps US, EEA/CH/UK and everywhere-else leads in three SEPARATE stores and returns only the one you name, so asking for the wrong one returns an empty file that reads exactly like "this ad got no leads". Run it once per region if the ad targets more than one. TikTok restricts lead download to ad-account ADMINS, so a refusal is a Business Center role rather than a broken connection and reconnecting will not fix it. Hermoso creates the download task, polls it and parses the CSV in ONE call because TikTok expires the generated file after 10 minutes. A task that has not finished, or a file TikTok zipped because it exceeded 10MB, is reported as "could not read" — never as "no leads". THE ROWS ARE PERSONAL DATA. Free.
| Argument | Type | Description |
|---|---|---|
| max | number | rows to return, default 200, cap 500. The `total` field always reports how many the file actually held. |
| adId | string | just that ad’s leads. Pass this OR pageId, never both — they are different scopes and TikTok would pick for you. |
| pageId | string | every lead on one Instant Form, from list_tiktok_ads_lead_forms |
| region* | string | REQUIRED. "us" = the United States · "eu" = the EEA, Switzerland and the UK · "other" = every country EXCEPT those — "other" is a third bucket, NOT "everywhere". Check the ad group’s targeting if unsure. |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
IMPORTANT: a domain can resolve to a DIFFERENT company than intended (e.g. Bala.com is an engineering firm, not the Bala fitness brand at shopbala.com). Before spending any credits on research or renders, VERIFY the returned `name` (and `summary`) match the brand the user meant; if it looks wrong, re-draft with the correct domain or a description (pass save:false until confirmed) — this tool cannot ask the user, so the caller owns that check.
| Argument | Type | Description |
|---|---|---|
| save | boolean | Default: saves only when NO brand is saved yet; pass true to REPLACE the saved profile (the drafted fields overwrite the saved ones), false to never save |
| domain | string | a website to scrape, or a social link (profile, YouTube channel or one post) to draft from the account it names |
| platform | string | socialHandle platform: instagram/tiktok/youtube/twitter/threads |
| confirmed | boolean | user confirmed it is their brand: read + save site product photos (paid) |
| description | string | a free-text brand description (no website) |
| socialHandle | string | a social handle (creators); needs platform, never guessed |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"save": {
"type": "boolean",
"description": "Default: saves only when NO brand is saved yet; pass true to REPLACE the saved profile (the drafted fields overwrite the saved ones), false to never save"
},
"domain": {
"type": "string",
"description": "a website to scrape, or a social link (profile, YouTube channel or one post) to draft from the account it names"
},
"platform": {
"type": "string",
"description": "socialHandle platform: instagram/tiktok/youtube/twitter/threads"
},
"confirmed": {
"type": "boolean",
"description": "user confirmed it is their brand: read + save site product photos (paid)"
},
"description": {
"type": "string",
"description": "a free-text brand description (no website)"
},
"socialHandle": {
"type": "string",
"description": "a social handle (creators); needs platform, never guessed"
}
}
}LIST a group of tools that is not in this session's roster. IT IS NOT HOW YOU REACH A TOOL — call_tool runs any Hermoso tool whether or not it is listed, and that works everywhere. Use this when the session will use MANY tools from one area and you want them in your list. WORKS ON CLIENTS THAT RE-READ THE TOOL LIST (stdio, the CLI, Cursor, Claude Code); a host that fixed its roster at connect time — claude.ai and ChatGPT do — will not show the new tools until it reconnects, and this tool says so in its reply rather than reporting a success you cannot use. The connect-time route that always works is `?tools=all` on the server URL. The default roster is CORE-FIRST: the core tools plus a few that make the connection drivable. Every other tool is held out of the LIST on SIZE alone — the whole registry is several hundred thousand tokens of schema re-sent on every turn, and a roster far past the 30-50 tool mark measurably degrades tool choice. The heaviest groups are `ads`, `analytics`, `channel_admin`: paid-campaign management across every ad platform is most of the total schema weight. NOTHING held out is unfinished or unsafe, and nothing is unreachable — find_tools finds it and call_tool runs it. CALL THIS WHEN A WHOLE AREA IS IN PLAY. If the user settles into building, budgeting, targeting or reporting on ad campaigns, call enable_tools({groups:['ads']}) and the tools appear. If they ask about their own site or product analytics, a tag/tracking container, or how a search engine crawls, indexes or ranks their site, call enable_tools({groups:['analytics']}). Groups: core, research, create, channels, channel_admin, analytics, ads, files, workspace — or 'all'. Free, instant, and it never turns anything off.
| Argument | Type | Description |
|---|---|---|
| groups* | array | Groups to switch on, e.g. ['ads']. Unknown names are refused by name rather than ignored. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groups"
],
"properties": {
"groups": {
"type": "array",
"items": {
"type": "string"
},
"description": "Groups to switch on, e.g. ['ads']. Unknown names are refused by name rather than ignored."
}
}
}One error group in full by fingerprint (from list_errors): every field, plus the most recent redacted occurrences — status, connector, job id, workspace, and a shape-only echo of the inputs. This is what makes a bug reproducible. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| fingerprint* | string | the `fp` value from list_errors |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fingerprint"
],
"properties": {
"fingerprint": {
"type": "string",
"description": "the `fp` value from list_errors"
}
}
}Ask Meta how many people a targeting spec can actually reach — BEFORE any budget is committed. Two ways: pass adSetId to size an ad set you already built (Meta uses its own saved targeting), or pass the same `targeting` object you would give create_meta_ad (plus optional objective / optimizationGoal / country / pixelId) to size an audience you are considering. Returns the monthly-active range, a daily-active estimate, and an explicit warning when the audience is too narrow to deliver. Free, read-only, creates nothing and spends nothing. Use it before recommending a budget and every time the user narrows a geo or piles on interests.
| Argument | Type | Description |
|---|---|---|
| adSetId | string | size an EXISTING ad set using its own saved targeting |
| country | string | 2-letter fallback country when targeting names no geo |
| pixelId | string | estimate a conversion goal against this pixel |
| objective | string | OUTCOME_TRAFFIC | OUTCOME_SALES | … — picks the matching optimization goal |
| targeting | object | a targeting object, same shape as create_meta_ad.targeting |
| adAccountId* | string | ad account id (act_… or digits) |
| conversionEvent |
Export the posting schedule to a Notion database, one row per post: date, channels, caption, media link and status. Creates a new database (under parentPageId, or private in the workspace) or writes into an existing one (databaseId), matching its columns by name. historyDays adds posts already published. Up to 100 rows per run; the answer says if more were left. Free.
| Argument | Type | Description |
|---|---|---|
| title | string | name for a new database |
| databaseId | string | an existing database id or link |
| historyDays | number | 0-90 days of published posts to include |
| dataSourceId | string | data source id of an existing database to write rows into, matched to columns by name |
| parentPageId | string | Notion page to create the new database under; omit for a private page in the workspace |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"title": {
"type": "string",
"description": "name for a new database"
},
"databaseId": {
"type": "string",
"description": "an existing database id or link"
},
"historyDays": {
"type": "number",
"description": "0-90 days of published posts to include"
},
"dataSourceId": {
"type": "string",
"description": "data source id of an existing database to write rows into, matched to columns by name"
},
"parentPageId": {
"type": "string",
"description": "Notion page to create the new database under; omit for a private page in the workspace"
}
}
}Turn a SWIPEFILE COLLECTION into a real Google Slides deck — one slide per saved ad, carrying the creative, the brand, the ad copy, the run dates with the run length, and the platform. This is the thing a marketer actually presents to a client or a team. Returns the presentation id + URL. Creates a NEW deck every time: under the drive.file scope Hermoso can only touch files it created, so it cannot add slides to a deck the user already has. A creative whose ad-library link has expired cannot be embedded — Meta signs those URLs with a short expiry — so that slide says so in words and keeps its copy and run dates, and the reply reports how many. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets, Docs and Slides).
| Argument | Type | Description |
|---|---|---|
| limit | number | max ads to include, 1-60 (default 30) |
| title | string | deck title (default: the collection name) |
| collection | string | the swipefile collection to export, by name or id (default: the first collection) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max ads to include, 1-60 (default 30)"
},
"title": {
"type": "string",
"description": "deck title (default: the collection name)"
},
"collection": {
"type": "string",
"description": "the swipefile collection to export, by name or id (default: the first collection)"
}
}
}Extend how long a post the authorized account owns stays promotable as a Spark Ad — and REGENERATE its code if it was deleted. THE DAYS ARE ADDED, NOT SET: TikTok's own example is that a post with 180 days remaining, extended by 180, ends up at 360 — so passing '365' to a post that already has time left does not mean 'expires in a year'. Must be one of 7, 30, 60, 180, 365. Ad authorization has to be ON already (set_tiktok_post_ad_authorization) or TikTok refuses. The reply reads the new window back, because TikTok's own response body is empty. NEEDS THE TIKTOK ACCOUNT AUTHORIZATION.
| Argument | Type | Description |
|---|---|---|
| itemId* | string | the TikTok post id |
| authorizationDays | number | 7 | 30 | 60 | 180 | 365 — ADDED to whatever is left, not set as an absolute. Default 30. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"itemId"
],
"properties": {
"itemId": {
"type": "string",
"description": "the TikTok post id"
},
"authorizationDays": {
"type": "number",
"description": "7 | 30 | 60 | 180 | 365 — ADDED to whatever is left, not set as an absolute. Default 30."
}
}
}Resolve a generated asset reference (a /generated/… path or any URL) to a clickable absolute URL + a direct download URL.
| Argument | Type | Description |
|---|---|---|
| url* | string | the asset url or /generated/ path |
| name | string | optional filename for the download |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the asset url or /generated/ path"
},
"name": {
"type": "string",
"description": "optional filename for the download"
}
}
}Ask Bingbot to fetch one URL now, so you can then read back what the crawler actually received with list_bing_webmaster_fetched_urls. THE FETCH IS QUEUED, NOT PERFORMED WHILE YOU WAIT — so "it has not appeared in the fetched list yet" is the normal FIRST answer, and it is reported as "requested, not yet done" rather than as a failure or as success. A URL that is not on this site is refused before anything is requested. 0 credits.
| Argument | Type | Description |
|---|---|---|
| url* | string | the full URL on THIS site for Bingbot to fetch |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the full URL on THIS site for Bingbot to fetch"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| query* | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"query": {
"type": "string",
"description": "one location name, or several comma-separated (up to 25)"
},
"countryCode": {
"type": "string",
"description": "2-letter hint, e.g. CA to disambiguate \"London\""
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
}
}
}Look up the Meta targeting ids you need before building an ad set — interests, behaviours, cities/regions/zips/DMAs, languages, employers, job titles and schools. type:"adinterest" (q:"yoga") returns interest ids + audience size; type:"adTargetingCategory" with class:"behaviors" returns behaviour ids; type:"adgeolocation" (q:"Toronto", optionally locationTypes:"city") returns the geo KEYS that go in targeting.geo.cities/regions/zips; type:"adlocale" (q:"french") returns language ids for targeting.locales. Read-only and free. Use it whenever the user names an audience in words — never guess an id.
| Argument | Type | Description |
|---|---|---|
| q | string | what to search for |
| type* | string | what kind of targeting object to search |
| class | string | for adTargetingCategory, e.g. "behaviors" or "interests" |
| limit | number | maximum matches to return; the reply shows up to 25 |
| adAccountId | string | search with that ad account’s token |
| countryCode | string | 2-letter hint to disambiguate a city name |
| locationTypes | string | comma-separated: country,region,city,zip,geo_market |
Search EVERY Hermoso tool — your starting list is deliberately short, and everything else in the product is here — by name, task or group. Each row gives the tool's PARAMETERS in one line, its CREDIT COST (free means free on every plan; a tool that runs a model quotes the live per-model figure) and its recent HEALTH on this server (failure rate and typical duration, or "no recent calls", which means unseen and not broken). Use it the moment the user asks for something you do not see a tool for (a campaign, an ad set, a lead form, a click-to-WhatsApp ad, a report, keywords, audiences): a tool missing from your list is NEVER proof the feature is missing. Then run the tool with call_tool. A tool that is failing or needs a connector this workspace has not made is ranked last and marked, never hidden — pass onlyHealthy:true if you want those left out. Free, read-only.
| Argument | Type | Description |
|---|---|---|
| group | string | limit to one group: core, research, create, channels, channel_admin, analytics, ads, files, workspace |
| limit | number | how many to return (default 12, max 40) |
| query | string | words from the task or the tool name, e.g. "lead form", "whatsapp", "google ads keyword", "meta insights" |
| onlyHealthy | boolean | leave out tools that are failing their recent calls or that need a connector this workspace has not made. Default false — nothing is hidden unless you ask, because a missing row reads as a missing capability. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"group": {
"type": "string",
"description": "limit to one group: core, research, create, channels, channel_admin, analytics, ads, files, workspace"
},
"limit": {
"type": "number",
"description": "how many to return (default 12, max 40)"
},
"query": {
"type": "string",
"description": "words from the task or the tool name, e.g. \"lead form\", \"whatsapp\", \"google ads keyword\", \"meta insights\""
},
"onlyHealthy": {
"type": "boolean",
"description": "leave out tools that are failing their recent calls or that need a connector this workspace has not made. Default false — nothing is hidden unless you ask, because a missing row reads as a missing capability."
}
}
}Delete a saved Memory item by its id (from list_memory). Records a cross-device delete so it doesn’t come back. Minor + re-creatable (you can remember it again), so no confirm needed.
| Argument | Type | Description |
|---|---|---|
| id* | string | the memory item id (from list_memory) |
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the memory item id (from list_memory)"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Make an exported sheet readable: bold the header row, FREEZE it so it stays visible while scrolling, and auto-size the columns so nothing is cut off. Worth calling right after create_sheet — a raw export with unsized columns and a header that scrolls away is the difference between a spreadsheet someone reads and one they close. Changes no cell VALUE, so it is never gated. The defaults do all three on the first tab; pass `tab` to pick another, freezeRows:0 to skip freezing, boldHeader:false or autoResize:false to skip those.
| Argument | Type | Description |
|---|---|---|
| tab | string | tab title or numeric sheetId (default: the first tab) |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| autoResize | boolean | auto-size columns to fit their content; default true, false skips it |
| boldHeader | boolean | bold the header row; default true, false skips it |
| freezeRows | number | how many top rows to freeze (default 1, 0 = none) |
| spreadsheetId | string | Google spreadsheet id; use this or sheetUrl |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"tab": {
"type": "string",
"description": "tab title or numeric sheetId (default: the first tab)"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"autoResize": {
"type": "boolean",
"description": "auto-size columns to fit their content; default true, false skips it"
},
"boldHeader": {
"type": "boolean",
"description": "bold the header row; default true, false skips it"
},
"freezeRows": {
"type": "number",
"description": "how many top rows to freeze (default 1, 0 = none)"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
}
}
}THE GTAG SNIPPET TO PASTE, plus what that web stream is actually set to collect. list_analytics_data_streams gives the measurement ID and stops one step short of what someone installing GA4 needs, which is the finished <script> block — Google returns it verbatim and marks it immutable. The same call answers the two questions that come next: whether ENHANCED MEASUREMENT is collecting scrolls, outbound clicks, site search, video, downloads, form interactions and single-page-app page views, and whether client-side REDACTION is stripping emails or query parameters out of recorded URLs. Read the master switch before believing a toggle: if enhanced measurement is off for the stream, every individual toggle is inert whatever it says. WEB STREAMS ONLY — all three resources are web-stream resources, and an app stream is refused by name with what it has instead. Any half that could not be read comes back null, which means unknown and not off. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| property* | string | NUMERIC GA4 property id, from list_analytics_properties |
| dataStream | string | stream id, measurement id or display name. Optional when the property has exactly one stream |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"property": {
"type": "string",
"description": "NUMERIC GA4 property id, from list_analytics_properties"
},
"dataStream": {
"type": "string",
"description": "stream id, measurement id or display name. Optional when the property has exactly one stream"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| id | string | Defaults to the ad account this Apple Ads connection resolved. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Defaults to the ad account this Apple Ads connection resolved."
}
}
}What Hermoso ALREADY KNOWS for the active profile: the same saved profile (products, logos, palette, positioning) + learned memory the web Studio uses. Call it when you need to know whether a brand is on file: if hasBrand is true you can omit brand everywhere; if false, onboard with draft_brand. Not a required first step before a render: the create tools read the saved brand by themselves. 0 credits.
| Argument | Type | Description |
|---|---|---|
| brand | string | profile id/name from list_brands, this call only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
}
}
}Fetch one Drive file’s metadata — name, type, size, modified time, a webViewLink to open it and a webContentLink to download it. Pass fileId (from list_drive_files). Read-only.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the Drive file id (from list_drive_files) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the Drive file id (from list_drive_files)"
}
}
}Whether the brand's lead-form leads are pushed into HubSpot AUTOMATICALLY: on or off, how often (everyMinutes), which platforms, when it runs next, and what the last run did — counts per platform, or why it did not run (HubSpot disconnected, a platform not readable). Read-only, free. Needs HubSpot connected.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Poll a render job by id. Returns status (queued|running|done|error), progress, and on done the served media URL. Renders take 1–3 minutes: keep calling this until done/error without asking the user — several calls is normal, not a stall. An id that does not exist on this account answers status "not_found" — that is FINAL: stop polling it, and do not re-fire the render (that double-charges).
| Argument | Type | Description |
|---|---|---|
| id* | string | The job id: starts with job_, from a render tool's jobId or from list_jobs |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "The job id: starts with job_, from a render tool's jobId or from list_jobs"
}
}
}Read a Meta AD ACCOUNT’s BILLING and DELIVERY state: account status in words (ACTIVE, DISABLED, UNSETTLED unpaid balance, IN_GRACE_PERIOD failed payment, PENDING_RISK_REVIEW, CLOSED…) with the disable reason; currency and time zone; BALANCE (the bill amount due); AMOUNT SPENT; SPEND CAP and how much is left under it (none means no account spending limit); the minimum daily budget; whether it is a PREPAID account; and the PAYMENT METHOD / FUNDING SOURCE (card, PayPal, prepaid funds / stored balance, invoice, credit line). Every amount is converted from Meta’s minor units into the account currency. Use it for “what is my Meta ad balance”, “how much prepaid funds are left”, “which card pays for my ads”, “why are my ads not delivering”, and before activating anything. IT CANNOT ADD PREPAID FUNDS OR CHANGE THE PAYMENT METHOD — Meta publishes no API for either, so the reply links the account’s page in Meta Billing (Ads Manager > Billing & payments) where the user does it; say so rather than looking for another tool. If Meta refuses the billing fields to this connection (the connected person lacks the MANAGE role on the account), the reply says the funding source could not be read, which is not the same as having none. Meta fields read: account_status, disable_reason, balance, amount_spent, spend_cap, min_daily_budget, is_prepay_account, funding_source_details. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adAccountId* | string | The ad account, with or without the act_ prefix |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"adAccountId": {
"type": "string",
"description": "The ad account, with or without the act_ prefix"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| names | array | narrow to some account property names, e.g. ["AIDetectedContentWatermarkPolicy"]; all of them by default |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"names": {
"type": "array",
"items": {
"type": "string"
},
"description": "narrow to some account property names, e.g. [\"AIDetectedContentWatermarkPolicy\"]; all of them by default"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}Fetch one OneDrive item’s metadata — name, type, size, modified time, a webViewLink to open it and a webContentLink to download it. Pass fileId (from list_onedrive_files). Read-only.
| Argument | Type | Description |
|---|---|---|
| fileId* | string | the OneDrive item id (from list_onedrive_files) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"fileId": {
"type": "string",
"description": "the OneDrive item id (from list_onedrive_files)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| audienceId* | string | ChatGPT Ads audience id, from list_openai_ads_audiences |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"audienceId": {
"type": "string",
"description": "ChatGPT Ads audience id, from list_openai_ads_audiences"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| audienceId* | string | audience the operation belongs to |
| operationId* | string | operation id returned by update_openai_ads_audience_members |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId",
"operationId"
],
"properties": {
"audienceId": {
"type": "string",
"description": "audience the operation belongs to"
},
"operationId": {
"type": "string",
"description": "operation id returned by update_openai_ads_audience_members"
}
}
}One Business Agent with its full configuration and publish state. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| businessAgentId* | string | business agent id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"businessAgentId"
],
"properties": {
"businessAgentId": {
"type": "string",
"description": "business agent id"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| feedId* | string | product feed id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"feedId"
],
"properties": {
"feedId": {
"type": "string",
"description": "product feed id"
}
}
}Whether SFTP bulk delivery is enabled for a ChatGPT Ads product feed, its connection URI and authentication method. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| feedId* | string | product feed id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"feedId"
],
"properties": {
"feedId": {
"type": "string",
"description": "product feed id"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId* | string | the ad, e.g. ad_… from list_openai_ads_campaigns with an adGroupId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adId"
],
"properties": {
"adId": {
"type": "string",
"description": "the ad, e.g. ad_… from list_openai_ads_campaigns with an adGroupId"
}
}
}One ChatGPT Ads lead form with its fields and revision ids (pass revisionId to read a specific revision). Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| leadFormId* | string | lead form id, from create_openai_ads_lead_form or the account |
| revisionId | string | read this revision instead of the latest one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"leadFormId"
],
"properties": {
"leadFormId": {
"type": "string",
"description": "lead form id, from create_openai_ads_lead_form or the account"
},
"revisionId": {
"type": "string",
"description": "read this revision instead of the latest one"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| assetId* | string | asset id from upload_reddit_ads_assets or list_reddit_ads_assets |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"assetId"
],
"properties": {
"assetId": {
"type": "string",
"description": "asset id from upload_reddit_ads_assets or list_reddit_ads_assets"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| uploadIds* | array | upload ids from upload_reddit_ads_assets, 1-50 per call |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"redditProfileId",
"uploadIds"
],
"properties": {
"uploadIds": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 50,
"minItems": 1,
"description": "upload ids from upload_reddit_ads_assets, 1-50 per call"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_…) — omit when only one is shared"
},
"redditProfileId": {
"type": "string",
"description": "the Reddit profile id (t2_…) from list_reddit_ads_profiles — assets hang off a profile, and it must be on the shared ad account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| importId | string | one import's id, for its issue rows and report link; omit for recent imports |
| catalogId* | string | catalog whose feed imports to read |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId"
],
"properties": {
"importId": {
"type": "string",
"description": "one import's id, for its issue rows and report link; omit for recent imports"
},
"catalogId": {
"type": "string",
"description": "catalog whose feed imports to read"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId* | string | the template ad id |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adId"
],
"properties": {
"adId": {
"type": "string",
"description": "the template ad id"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}Read this account's app settings — the LANGUAGE Hermoso writes ads and copy in (chat replies follow the language the user writes in), the app appearance (theme), and whether the weekly competitor-watch email is on. Same settings as the web app's Settings pane. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Load a bundled skill’s full SKILL.md workflow instructions by name (from list_skills), or a built-in in-app skill by its id (e.g. youtube_script, retention_skeleton). Follow the loaded instructions to run that workflow with the other tools. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| name* | string | bundle name from list_skills, e.g. hermoso-generate |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "bundle name from list_skills, e.g. hermoso-generate"
}
}
}How much money the brand's Stripe PAYMENT LINKS made — the "what did the link in this ad earn" question. Paid purchases and revenue (MAJOR units, per currency) from completed checkouts between since and until (YYYY-MM-DD, default the last 30 days), for one paymentLinkId or every link. groupBy: link (default without an id — which link earned most), reference (default with an id — per ad, from the referenceId a taggedUrl carries), promotion_code, or day. For a subscription link only the first payment counts; renewals are in stripe_report. Says when the read is capped. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| since | string | start date, YYYY-MM-DD; default 30 days before until |
| until | string | end date, YYYY-MM-DD; default today |
| groupBy | string | link, reference, promotion_code or day; default link without paymentLinkId, reference with it |
| paymentLinkId | string | one payment link, plink_...; omit for every link |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"since": {
"type": "string",
"description": "start date, YYYY-MM-DD; default 30 days before until"
},
"until": {
"type": "string",
"description": "end date, YYYY-MM-DD; default today"
},
"groupBy": {
"enum": [
"link",
"reference",
"promotion_code",
"day"
],
"type": "string",
"description": "link, reference, promotion_code or day; default link without paymentLinkId, reference with it"
},
"paymentLinkId": {
"type": "string",
"description": "one payment link, plink_...; omit for every link"
}
}
}What content this TikTok ad account’s ads are allowed to appear next to — the inventory filter tier (EXPANDED / STANDARD / LIMITED / NO_BRAND_SAFETY) plus the suitability controls: which content categories are excluded and which vertical’s sensitive content is avoided. It also returns, free and in the same call, THE CATALOGUE of every category id you could set, because those ids are unguessable and there is otherwise no way to act on the setting. Warning: THE SCOPE IS THE THING TO KNOW: TikTok applies this to future Smart+ campaigns and, in their own words, "will not apply to future regular campaigns created using /campaign/create/" — which is the endpoint create_tiktok_ads_campaign uses. So this is the ad account default and the Ads Manager setting, NOT a guarantee inherited by a campaign Hermoso builds. A NULL vertical-sensitivity catalogue means this advertiser is not on TikTok’s allowlist for that feature, never that no vertical categories exist. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| categories | boolean | default true — set false to skip the category catalogue and read only the current setting |
| advertiserId | string | from list_tiktok_ads_accounts |
| objectiveType | string | which objective to look the catalogue up under — default REACH. TikTok supports only these three on the catalogue endpoint. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"categories": {
"type": "boolean",
"description": "default true — set false to skip the category catalogue and read only the current setting"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"objectiveType": {
"enum": [
"REACH",
"VIDEO_VIEWS",
"ENGAGEMENT"
],
"type": "string",
"description": "which objective to look the catalogue up under — default REACH. TikTok supports only these three on the catalogue endpoint."
}
}
}Read the Spark-Ads authorization status of a post the authorized account owns — whether it is promotable, its authorization CODE, and the window the authorization runs for. A SPARK AD CANNOT OUTLIVE ITS AUTHORIZATION, so check the end time before building a campaign around a post. TIKTOK ERRORS RATHER THAN ANSWERING when a post has no authorization code at all, so a not-found style refusal from here almost always means Ad authorization was never turned on (or the code was deleted) — turn it on with set_tiktok_post_ad_authorization — and not that anything is broken. NEEDS THE TIKTOK ACCOUNT AUTHORIZATION. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| itemId* | string | the TikTok post id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"itemId"
],
"properties": {
"itemId": {
"type": "string",
"description": "the TikTok post id"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| videoId* | string | the creator's video the authorization was requested for |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"videoId"
],
"properties": {
"videoId": {
"type": "string",
"description": "the creator's video the authorization was requested for"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
}
}
}Read the leads submitted to an X lead form over a window (default the last 30 days) — each lead's answers keyed by field (EMAIL, FULL_NAME…). X builds the export as a job; this waits up to ~45s and, if X is still working, returns an exportId to collect later with no new job. X lets only an ACCOUNT ADMIN export leads, and refuses an ARCHIVED form, so export before archiving. format csv/xlsx returns a download link instead. The result is personal information. Free.
| Argument | Type | Description |
|---|---|---|
| days | number | window length when startTime is omitted; default 30 |
| format | string | json (default, returns leads inline) | csv | xlsx (returns a download link) |
| endTime | string | ISO 8601; default now |
| exportId | string | collect an export that was still running |
| accountId | string | older name for adAccountId (MCP only) |
| startTime | string | ISO 8601 |
| leadFormId | string | X lead form id from list_x_ads_lead_forms; required unless exportId is given |
| adAccountId | string |
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.
| Argument | Type | Description |
|---|---|---|
| limit | number | rows, max 10000 — Google’s own ceiling, and the clamp is reported |
| since | string | YYYY-MM-DD, default 14 days ago |
| until | string | YYYY-MM-DD, default today |
| detail | boolean | change_event only — include the old/new resource snapshots |
| source | string | default change_event (30 days, field-level). change_status is 90 days and is the only one that sees Google Ads Editor. |
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.
| Argument | Type | Description |
|---|---|---|
| url | string | one page to mine for ideas |
| site | string | a whole domain to mine, e.g. example.com |
| limit | number | how many ideas to return (1–200, default 50) |
| network | string | default GOOGLE_SEARCH |
| keywords | array | up to 20 seed terms |
| language | string | ISO code, e.g. "en" |
| locations | array | place NAMES, e.g. ["United States"] |
Ask Google whether a Business Profile listing can run LOCAL SERVICES ads on this Google Ads account: eligible or not, why not, and which lead types and categories it qualifies for. Read-only.
| Argument | Type | Description |
|---|---|---|
| listingId* | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"listingId"
],
"properties": {
"listingId": {
"type": "string",
"description": "numeric Business Profile location id"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok); omit to use the brand’s selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}WHY Google rejected or limited something: every DISAPPROVED or APPROVED_LIMITED ad, asset (headlines, images, sitelinks, Performance Max assets) and keyword on the account or one campaign, with Google’s policy topic, its effect, the evidence (offending text, a broken landing page and its HTTP code, restricted countries) and appeal state. includeApproved lists everything. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| campaignId | string | only this campaign |
| customerId | string | 10-digit account id, omit for the brand’s default account |
| includeApproved | boolean | list approved and pending items too |
| loginCustomerId | string | manager id if operating through an MCC |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"campaignId": {
"type": "string",
"description": "only this campaign"
},
"customerId": {
"type": "string",
"description": "10-digit account id, omit for the brand’s default account"
},
"includeApproved": {
"type": "boolean",
"description": "list approved and pending items too"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}Google Ads’ own advice for the account: the optimization score (account and per campaign), every open recommendation with what applying it would change (budget $X -> $Y, a keyword, a bid target, a Google-written ad) and Google’s weekly impact estimate, each tagged spend (changes what the account buys) or serves (changes what an ad shows), plus which types Google is auto-applying by itself. An 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.
| Argument | Type | Description |
|---|---|---|
| types | array | recommendation types, e.g. CAMPAIGN_BUDGET, KEYWORD; an unknown one is refused by name |
| campaignId | string | only recommendations for this campaign |
| customerId | string | 10-digit account id, omit for the brand’s default account |
| loginCustomerId | string | manager id if operating through an MCC |
| includeDismissed | boolean | also show dismissed ones |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"types": {
"type": "array",
"items": {
"type": "string"
},
"description": "recommendation types, e.g. CAMPAIGN_BUDGET, KEYWORD; an unknown one is refused by name"
},
"campaignId": {
"type": "string",
"description": "only recommendations for this campaign"
},
"customerId": {
"type": "string",
"description": "10-digit account id, omit for the brand’s default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"includeDismissed": {
"type": "boolean",
"description": "also show dismissed ones"
}
}
}Use this when the user asks how their Google Ads campaigns performed. Run a GAQL (Google Ads Query Language) report for detailed performance breakdowns — ad groups, ads, keywords, search terms, demographics, geo. Pass customerId + a GAQL query (SELECT … FROM <resource> WHERE segments.date DURING LAST_30_DAYS). Allowed FROM resources: campaign, ad_group, ad_group_ad, keyword_view, campaign_budget, 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.
| Argument | Type | Description |
|---|---|---|
| query* | string | GAQL, e.g. "SELECT ad_group.name, metrics.clicks, metrics.cost_micros FROM ad_group WHERE segments.date DURING LAST_7_DAYS" |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"query": {
"type": "string",
"description": "GAQL, e.g. \"SELECT ad_group.name, metrics.clicks, metrics.cost_micros FROM ad_group WHERE segments.date DURING LAST_7_DAYS\""
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}Probe what this Hermoso account can do RIGHT NOW: available image/video model ids + their credit costs (estimates: a render is charged its actual cost), aspect ratios, video durations, the recipe ids, and the canEdit/canAvatar flags. Call it when you need a specific model id, a credit cost, or a capability you are not sure of. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Return the credit balance, recent priced calls with their credits, and costModel (the one-sentence rule of what costs credits). THE RULE: only AI model runs and Ad Spy research spend credits; publishing, scheduling, ads management, analytics, comments, DMs and connectors cost no credits on any plan (X is the single per-call exception; xCost gives its exact credits per post, with and without a link). Check before kicking off paid generation; answer "does posting cost credits?" with NO. HOW MANY posts is set by the plan, not credits: the Free plan includes 10 posts a month on up to 3 channels, and a post counts once however many channels it goes to (`posting` says how many are used); every paid plan posts without a limit and runs autopilot (Basic $9.99/mo: unlimited posting and Autopilot, no monthly credits; it includes the same 250+ free credits as the Free plan, monthly credits start on Starter, and Autopilot spends credits). Starter, Pro and Studio: media models at cost + 10% (memberRate); Basic and Free pay standard prices.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Deals from the brand's HubSpot created in the window (default the last 90 days): count and amount by stage (labels resolved through the deal pipelines, closed-won recognised from the stage's own metadata), by original source, BY CAMPAIGN (source -> platform -> campaign from HubSpot's Original Source drill-downs, so PAID_SOCIAL splits into the actual campaigns), BY OWNER (the sales rep holding each deal, by name), a monthly created / closed-won series, and totals. Capped at 2,000 deals and says so. Read-only, free. Needs HubSpot connected.
| Argument | Type | Description |
|---|---|---|
| since | string | YYYY-MM-DD, default 90 days ago |
| until | string | YYYY-MM-DD, default today |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"since": {
"type": "string",
"description": "YYYY-MM-DD, default 90 days ago"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD, default today"
}
}
}Pull the files in a Google Drive or OneDrive FOLDER into this profile's Library, so they can be used like anything rendered here — published, scheduled, cloned, used as a product photo or a reference. Hermoso downloads each file with the user's own connected account (a Drive/OneDrive file is not public) and stores a durable Hermoso url for each. Give `folderId` from list_drive_files / list_onedrive_files with onlyFolders — omit it for the root. GOOGLE DRIVE ONLY SHOWS WHAT THE USER HANDED OVER: our Drive scope is `drive.file`, so Hermoso can see the files and folders it created plus the ones the user picked with the Google picker in the app, and NEVER their whole Drive. Picking a FOLDER hands over the folder alone, not the files in it, so if a folder comes back empty the reply carries a link that opens the picker inside it, where the user selects every file at once (Cmd+A / Ctrl+A); give them that link. SUBFOLDERS ARE NOT WALKED and Google-native docs (Docs/Sheets/Slides) have no file to download: both are reported back BY NAME rather than silently dropped, along with anything too large or unreadable, so you can tell the user exactly what did and did not come across.
| Argument | Type | Description |
|---|---|---|
| limit | number | how many files to bring across in this call (default 10, max 25). Anything over the limit is listed as skipped so you know what is left. |
| folderId | string | the folder to import, from list_drive_files / list_onedrive_files (onlyFolders:true). Omit for the root of the drive. |
| provider* | string | which cloud — `drive` is Google Drive, `onedrive` is Microsoft OneDrive |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"limit": {
"type": "number",
"description": "how many files to bring across in this call (default 10, max 25). Anything over the limit is listed as skipped so you know what is left."
},
"folderId": {
"type": "string",
"description": "the folder to import, from list_drive_files / list_onedrive_files (onlyFolders:true). Omit for the root of the drive."
},
"provider": {
"enum": [
"drive",
"onedrive"
],
"type": "string",
"description": "which cloud — `drive` is Google Drive, `onedrive` is Microsoft OneDrive"
}
}
}Invite someone to this profile by email (role: member = read-only on billing, admin = full). This SENDS a real email invite / share link — an account change. Confirm the exact email + role with the user, then call with confirm:true.
| Argument | Type | Description |
|---|---|---|
| role | string | default member |
| email* | string | the invitee’s email |
| confirm | boolean | REQUIRED true — this invites a real person |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email"
],
"properties": {
"role": {
"enum": [
"member",
"admin"
],
"type": "string",
"description": "default member"
},
"email": {
"type": "string",
"description": "the invitee’s email"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — this invites a real person"
}
}
}On a connector that several teammates can contribute their OWN account to (see list_connectors — the row reports multiContributor), remove YOURS from this profile: your stored credential is dropped and the accounts you shared stop being shared. Your teammates' accounts on the same connection keep working, and nothing changes at the provider — reconnecting in a browser shares again. Use this instead of disconnect_connector when the connection is not yours to remove: disconnect_connector revokes the grant at the provider and only the person who created the connection may call it.
| Argument | Type | Description |
|---|---|---|
| provider* | string | provider id exactly as list_connectors reports it, e.g. "linkedin", "tiktok_ads", "meta" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"provider": {
"type": "string",
"description": "provider id exactly as list_connectors reports it, e.g. \"linkedin\", \"tiktok_ads\", \"meta\""
}
}
}LINK a GA4 property to a Google Ads account. This is the prerequisite for two things users constantly ask for and cannot otherwise have: importing GA4 key events as Google Ads CONVERSIONS — which is what gets a smart-bidding campaign past the conversion-tracking refusal for a GA4-first advertiser — and using a GA4 AUDIENCE for remarketing. RE-LINKING AN ACCOUNT THAT IS ALREADY LINKED IS NOT AN ERROR: it reports that and changes nothing, because this is exactly the call an agent makes when something downstream is not working. adsPersonalizationEnabled DEFAULTS TO TRUE deliberately — a link with it off is the silent dead end where the audience exists in GA4 and never appears in Google Ads, with nothing reporting an error. The connected Google account needs EDIT on the GA4 property AND admin on the Google Ads account; without the second Google refuses and the refusal reads like a Hermoso fault. Google can take 24-48 hours before an audience shows up on the other side. 0 credits.
| Argument | Type | Description |
|---|---|---|
| property* | string | the NUMERIC GA4 property id from list_analytics_properties |
| customerId* | string | the 10-digit Google Ads customer id shown top-right in Google Ads; dashes optional |
| adsPersonalizationEnabled | boolean | defaults to TRUE. Only set false for measurement-only linking — it disables remarketing |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"customerId"
],
"properties": {
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties"
},
"customerId": {
"type": "string",
"description": "the 10-digit Google Ads customer id shown top-right in Google Ads; dashes optional"
},
"adsPersonalizationEnabled": {
"type": "boolean",
"description": "defaults to TRUE. Only set false for measurement-only linking — it disables remarketing"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| action | string | default LINK; a second LINK is a REMINDER |
| videoId* | string | the creator’s public video: from list_tiktok_tto_campaigns or tiktok_tto_campaign_report |
| campaignId* | string | TikTok One campaign the video is linked to, from list_tiktok_tto_campaigns |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"videoId"
],
"properties": {
"action": {
"enum": [
"LINK",
"REVOKE"
],
"type": "string",
"description": "default LINK; a second LINK is a REMINDER"
},
"videoId": {
"type": "string",
"description": "the creator’s public video: from list_tiktok_tto_campaigns or tiktok_tto_campaign_report"
},
"campaignId": {
"type": "string",
"description": "TikTok One campaign the video is linked to, from list_tiktok_tto_campaigns"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
}
}
}LinkedIn ad performance — impressions, clicks, cost, website conversions, leads and social actions — pivoted by CAMPAIGN (default), CAMPAIGN_GROUP, CREATIVE, ACCOUNT, CONVERSION, PLACEMENT_NAME, IMPRESSION_DEVICE_TYPE, SERVING_LOCATION… or by AUDIENCE DEMOGRAPHICS: MEMBER_COMPANY_SIZE, MEMBER_INDUSTRY, MEMBER_SENIORITY, MEMBER_JOB_TITLE, MEMBER_JOB_FUNCTION, MEMBER_COUNTRY_V2, MEMBER_REGION_V2, MEMBER_COMPANY. The MEMBER_* pivots report job title, seniority, company size and the other member demographics. LinkedIn allows exactly ONE pivot per report, so ask for them one at a time and join the answers yourself. An unknown pivot or granularity is refused BY NAME rather than forwarded. On a demographic pivot LinkedIn returns only the top 100 values, DROPS any value under 3 events (so the rows will not sum to the campaign total) and lags 12–24 hours behind the performance numbers — the note says so, every time. Window via since/until (YYYY-MM-DD). ZERO rows genuinely means no delivery in that window; say exactly that and never present zeros as measured performance. A LinkedIn TEST ad account NEVER returns analytics, and the note says so when that is what you are looking at. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| pivot | string | CAMPAIGN (default), CAMPAIGN_GROUP, CREATIVE, ACCOUNT, CONVERSION, PLACEMENT_NAME, IMPRESSION_DEVICE_TYPE, SERVING_LOCATION, or one MEMBER_* demographic pivot — an unknown value is refused with the full list |
| since | string | YYYY-MM-DD, default 30 days ago |
| until | string | YYYY-MM-DD |
| fields | array | metric names — omit for the standard set (LinkedIn returns ONLY impressions and clicks if none are named) |
HOW MANY LINKEDIN MEMBERS a targeting spec reaches, before any budget is committed — the cheapest sanity check there is on a B2B audience, and it needs no ad account. Pass locations plus optional include:{titles, industries, seniorities, staffCountRanges, jobFunctions, skills, …}; search_linkedin_ads_targeting resolves any of those names to the URNs LinkedIn demands, free. THE CRITICAL THING TO SAY WHEN REPORTING: a returned total of 0 means the audience is UNDER 300 PEOPLE, not that it is empty — LinkedIn suppresses any count below 300 to protect member privacy, and 300 is also the minimum audience a campaign may run against, so a 0 means this targeting is too narrow to advertise to. The figure is a rounded approximation, so quote it as an estimate and never as a headcount. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| include | object | more facets ANDed onto locations, e.g. {titles:[…], industries:[…], seniorities:[…], staffCountRanges:[…]} |
| locations | array | geo URNs or bare geo ids, e.g. ["103644278"] for the United States |
| targetingCriteria | object | LinkedIn’s raw targeting object — overrides locations/include |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"include": {
"type": "object",
"description": "more facets ANDed onto locations, e.g. {titles:[…], industries:[…], seniorities:[…], staffCountRanges:[…]}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "geo URNs or bare geo ids, e.g. [\"103644278\"] for the United States"
},
"targetingCriteria": {
"type": "object",
"description": "LinkedIn’s raw targeting object — overrides locations/include",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}LinkedIn’s OWN suggested bid and daily-budget range for a specific audience — the suggested bid with a low/mid/high range, the hard bid limits, and the minimum, default and maximum daily budget, all in the ad account’s currency. Use it before proposing a number to a user instead of guessing what LinkedIn costs, and pair it with linkedin_audience_count to answer "can we afford this audience?" in one go. Below LinkedIn’s minimum bid it says delivery "may be poor" for Sponsored Update campaigns and is impossible for every other format. These are ESTIMATES for this audience, not prices, and nothing is committed until a campaign is activated with set_linkedin_ads_status(confirm:true). Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| bidType | string | default CPM |
| include | object | targeting facets ANDed in, e.g. {titles, industries}; same shape as create_linkedin_ads_campaign include |
| currency | string | ISO currency code for the estimate, e.g. USD |
| locations | array | geo URNs or ids for the audience, from search_linkedin_ads_targeting |
| matchType | string | default EXACT |
| adAccountId | string | LinkedIn ad account id; its currency is the default for the estimate |
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.
| Argument | Type | Description |
|---|---|---|
| end | string | ISO 8601 |
| start | string | ISO 8601, e.g. '2026-08-01T00:00:00+00:00' |
| chartId | string | only annotations shown on this chart — does not combine with category |
| category | string | category NAME — does not combine with chartId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"end": {
"type": "string",
"description": "ISO 8601"
},
"start": {
"type": "string",
"description": "ISO 8601, e.g. '2026-08-01T00:00:00+00:00'"
},
"chartId": {
"type": "string",
"description": "only annotations shown on this chart — does not combine with category"
},
"category": {
"type": "string",
"description": "category NAME — does not combine with chartId"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| category | string | default "event" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"category": {
"enum": [
"category",
"event",
"event-property",
"user-property",
"group-property"
],
"type": "string",
"description": "default \"event\""
}
}
}The GA4 audiences (remarketing lists) on a property, WITH whether they can actually be used in Google Ads — it reads the Google Ads link state alongside them, because an audience on an unlinked property is a silent dead end that reports no error anywhere. ARCHIVED audiences are not listed, so absence here is not proof one never existed. GA4's PREDEFINED audiences ('All Users', 'Purchasers') ARE returned — verified live against a real property — and they come back with clauseCount 0, because Google defines them internally rather than with filter clauses; that is normal and is not a broken audience. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| property* | string | the NUMERIC GA4 property id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id"
}
}
}The GA4 data streams on a property, with the measurement ID (G-...) each one carries and how many Measurement Protocol secrets it already has. THIS IS WHERE A GTAG OR GTM INSTALL GETS ITS ID, and it is the first call before sending any server-side event. A web stream has a measurementId; an app stream has none and uses its firebaseAppId instead, so do not report an app stream as broken for having no G- id. A stream whose secret count could not be read comes back as null, which means unknown and not zero. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| property* | string | NUMERIC GA4 property id, from list_analytics_properties |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"property": {
"type": "string",
"description": "NUMERIC GA4 property id, from list_analytics_properties"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| property* | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name"
}
}
}Which Google Ads accounts a GA4 property is linked to, and whether each link has PERSONALIZED ADVERTISING on — the flag that decides whether GA4 audiences and remarketing events reach Google Ads at all. RUN THIS BEFORE two claims: that a GA4 audience can be remarketed to, and that a Google Ads campaign's missing conversion data is a Google Ads problem. For a GA4-first advertiser the conversions come from this link, so an unlinked property is the usual cause and no error anywhere says so. Read-only, 0 credits. Needs Google Analytics connected.
| Argument | Type | Description |
|---|---|---|
| property* | string | the NUMERIC GA4 property id from list_analytics_properties — never the G-XXXXXXX Measurement ID |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties — never the G-XXXXXXX Measurement ID"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| 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 |
| property* | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"search": {
"type": "string",
"description": "substring filter over api name AND human label, e.g. \"revenue\", \"campaign\", \"landing\" — strongly recommended, since the unfiltered vocabulary is several hundred entries"
},
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name"
}
}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Ad groups on the brand’s Apple Ads account — status, the system-computed displayStatus, pricing model, the bid off the ad group’s bidStrategy, and whether Search Match (Apple’s automated keyword matching) is on. campaignId is OPTIONAL: with it, the ad groups of that one campaign; WITHOUT it, every ad group in the ad account, each row carrying its own campaignId. (Apple’s older API had no account-wide ad-group list and required the campaign id; its Platform API does, and this now uses it.) Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of ad groups to return |
| offset | number | ad groups to skip, for paging |
| campaignId | string | optional — from list_apple_ads_campaigns. Omit for every ad group in the ad account. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of ad groups to return"
},
"offset": {
"type": "number",
"description": "ad groups to skip, for paging"
},
"campaignId": {
"type": "string",
"description": "optional — from list_apple_ads_campaigns. Omit for every ad group in the ad account."
}
}
}The ads on this Apple Ads account — the serving units that put a creative into an ad group. Each row carries the advertiser status, the rolled-up displayStatus, Apple’s own systemStatus and, when an ad is not serving, systemStatusReasons saying exactly why: AD_APPROVAL_PENDING while Apple reviews it, CREATIVE_PENDING, CREATIVE_LOCALE_INCOMPATIBLE, PRODUCT_PAGE_HIDDEN, PAUSED_BY_USER and more. READ THAT FIELD BEFORE CONCLUDING ANYTHING IS BROKEN — a brand-new ad awaiting Apple’s review is NOT_RUNNING by design, and systemStatusLimitingReasons means it IS running but throttled. Filter by adGroupId, campaignId, creativeId or status, or pass id for one. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| id | string | Fetch one ad. |
| limit | number | maximum number of ads to return |
| offset | number | ads to skip, for paging |
| status | string | advertiser's own setting: ENABLED or PAUSED |
| adGroupId | string | only ads in this ad group |
| campaignId | string | only ads in this campaign |
| creativeId | string | only ads that use this creative |
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.
| Argument | Type | Description |
|---|---|---|
| resourceType* | string | REQUIRED. CONTENT_PROVIDER = App Store (CPID); BUSINESS_BRAND = Apple Maps (Brand ID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"resourceType"
],
"properties": {
"resourceType": {
"enum": [
"CONTENT_PROVIDER",
"BUSINESS_BRAND"
],
"type": "string",
"description": "REQUIRED. CONTENT_PROVIDER = App Store (CPID); BUSINESS_BRAND = Apple Maps (Brand ID)."
}
}
}Images in the Apple Ads asset library with format, orientation, dimensions and eligibility — use it to check whether an uploaded asset has finished processing before referencing it in a creative. Filter by mapsBrandId (Apple’s promotedObjectId), promotedObjectType or assetType, or pass id for one. CROPS AND OTHER VARIANTS NEVER APPEAR IN A LIST: Apple omits them from query results, so a variant has to be fetched by its own id. Querying covers both App Store apps and Apple Maps brands even though only Maps assets can be uploaded. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| id | string | Fetch one asset — the only way to see a crop or variant. |
| limit | number | maximum number of assets to return |
| offset | number | assets to skip, for paging |
| brandId | string | older name for mapsBrandId (MCP only) |
| assetType | string | asset type; IMAGE is the only one listed |
| mapsBrandId | string | Scope to one brand or app (promotedObjectId). |
| promotedObjectType | string | APPSTORE_APP or BUSINESS_BRAND: which kind of promoted object the assets belong to |
The Apple Maps brands on this Apple Ads account. A BRAND IS THE ROOT OF EVERY ADS-ON-APPLE-MAPS CAMPAIGN and its id is what three other calls need: it is the promotedObjectId of a BUSINESS_BRAND campaign, the mapsBrandId upload_apple_ads_asset uploads against, and the mapsBrandId a LOCAL_ADS_SEARCH_CREATIVE carries. A brand must read eligibility ELIGIBLE before Apple will let a campaign use it. YOU CANNOT CREATE A BRAND HERE and that is Apple’s design, not a gap: brands come from Apple Business Connect, and Ads on Apple Maps additionally need the ad account to carry productFeatures BUSINESS_BRAND_MANUAL plus a BUSINESS_BRAND delegation, neither of which any API can grant. AN EMPTY LIST IS THE NORMAL ANSWER FOR AN APP STORE ADVERTISER and does not mean anything is broken — App Store campaigns need no brand at all. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| name | string | brand name to filter by |
| limit | number | maximum number of brands to return |
| offset | number | brands to skip, for paging |
| brandId | string | older name for mapsBrandId (MCP only) |
| mapsBrandId | string | Fetch one brand by id. |
| countryOrRegion | string | Two-letter code, e.g. "US". |
| eligibilityStatus |
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.
| Argument | Type | Description |
|---|---|---|
| id | string | Fetch one budget order, including its invoice details and assigned ad accounts. |
| limit | number | maximum number of budget orders to return |
| offset | number | budget orders to skip, for paging |
| filters | array | Query conditions, e.g. [{"field":"name","operator":"CONTAINS","value":"Q3"}]. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Fetch one budget order, including its invoice details and assigned ad accounts."
},
"limit": {
"type": "number",
"description": "maximum number of budget orders to return"
},
"offset": {
"type": "number",
"description": "budget orders to skip, for paging"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "Query conditions, e.g. [{\"field\":\"name\",\"operator\":\"CONTAINS\",\"value\":\"Q3\"}]."
}
}
}Read the brand’s connected Apple Ads (Apple Search Ads) campaigns — App Store search ads, on Apple’s Platform API. Each row carries status, the system-computed displayStatus, the daily budget, the bid strategy, the countries and placements it runs in and, when a campaign cannot run, systemStatusReasons stating exactly why. Read-only, free, zero spend risk. Needs Apple Ads connected (Settings > Connectors > Apple Ads, or connect_connector): there is no OAuth — Hermoso generates an EC signing key, the user pastes the public half into Apple Ads > Account Settings > API and pastes back clientId / teamId / keyId. To BUILD on this account, use create_apple_ads_campaign / create_apple_ads_ad_group / add_apple_ads_keywords: everything is created PAUSED and only set_apple_ads_status(confirm:true) can arm spend.
| Argument | Type | Description |
|---|---|---|
| limit | number | page size, default 100, Apple’s max is 1000 |
| offset | number | offset pagination |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "page size, default 100, Apple’s max is 1000"
},
"offset": {
"type": "number",
"description": "offset pagination"
}
}
}The ad creatives on this Apple Ads account, each with its type, the app and product page it points at, its systemStatus (VALID / INVALID / PENDING) and its eligibility per ad placement. A creative is reusable — one can back ads in several ad groups and campaigns. Filter by creativeType, systemStatus, a name prefix or eligibility, pass id for a single one, or includeDeleted:true to see deleted records (Apple excludes them by default). AN EMPTY LIST IS NORMAL on an account that only runs classic Search Results campaigns: those need no creative at all, because Apple renders the App Store product page itself. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| id | string | Fetch one creative. |
| name | string | Name prefix. |
| limit | number | maximum number of creatives to return |
| offset | number | creatives to skip, for paging |
| eligibility | string | ELIGIBLE or INELIGIBLE for ad placement |
| creativeType | string | creative kind: DEFAULT_PRODUCT_PAGE, CUSTOM_PRODUCT_PAGE or LOCAL_ADS_SEARCH_CREATIVE |
| systemStatus | string | Apple's review state: VALID, INVALID or PENDING |
Targeting or negative keywords with their match type, status and bid. THE TWO RESOURCES SCOPE DIFFERENTLY and Apple enforces it: TARGETING keywords (the default) need campaignId OR adGroupId — campaignId alone now returns every targeting keyword in the campaign, which Apple’s older API had no way to ask for. NEGATIVE keywords (negative:true) always need one of the two as well, and Apple splits them by LEVEL: campaignId alone returns the CAMPAIGN-level negatives that suppress the whole campaign (scope ‘campaign’, the default), scope ‘adgroup’ returns every AD-GROUP-level negative across that campaign in one call, and naming an adGroupId returns just that ad group’s. The reply states which level it counted, because the row shapes are identical and the number means different things. There is no account-wide keyword list. Apple allows up to 5000 keywords per ad group. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of keywords to return |
| scope | string | NEGATIVE keywords only, with campaignId and no adGroupId: "campaign" (default) = the campaign-level negatives; "adgroup" = every ad-group-level negative across the campaign. |
| offset | number | keywords to skip, for paging |
| negative | boolean | list negative keywords instead of targeting keywords |
| adGroupId | string | narrow to one ad group. Outranks scope on both resources. |
The location groups on this Apple Ads account — named sets of a brand’s shops that an ad group targets. Each row carries its type (STATIC or DYNAMIC), Apple’s systemStatus and groupTotal, the number of locations currently in it. READ groupTotal BEFORE TARGETING A GROUP: a DYNAMIC group sits at PENDING while Apple evaluates its rules, and a group that evaluated to ZERO is Apple’s one silent failure here — it reports no error, so a rule spelled "CA" instead of "California" builds an empty group that looks fine. Soft-deleted groups are excluded unless you pass includeDeleted. There is no filter for "which groups contain this location": query by mapsBrandId and inspect membership. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| id | string | Fetch one group. |
| name | string | Substring match. |
| limit | number | maximum number of location groups to return |
| offset | number | groups to skip, for paging |
| brandId | string | older name for mapsBrandId (MCP only) |
| groupType | string | STATIC (fixed shop list) or DYNAMIC (rule-based) |
| mapsBrandId | string | Apple Maps brand id from list_apple_ads_brands |
The physical business locations behind an Apple Maps brand — the shops, restaurants or service centres an Ads-on-Apple-Maps campaign promotes. Filter by mapsBrandId (do: without it Apple returns every location across every brand, which can be a very large page), plus status, eligibility or address. LOCATIONS ARE READ-ONLY THROUGH THIS API BY APPLE’S DESIGN: Apple Business Connect creates and maintains them, so a missing shop, a wrong address or a closure is fixed there and appears here afterwards — there is no create, update or delete to look for. Only a location with status OPEN and eligibility ELIGIBLE is targetable. You do not target locations directly: collect the ids you want, put them in a location group with create_apple_ads_location_group, then point an ad group at that group. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| id | string | Fetch one location. |
| name | string | location name to filter by |
| limit | number | maximum number of locations to return |
| offset | number | locations to skip, for paging |
| status | string | Usually OPEN — only OPEN locations are targetable. |
| brandId | string | older name for mapsBrandId (MCP only) |
| locality |
The Apple Ads ad accounts these credentials can act as, with the roles held on each, plus the organization’s currency, time zone and payment model. NAMING: Apple’s older API called these “organizations” (campaign groups) and its Platform API calls them AD ACCOUNTS — the id is the same number, and `orgId` and `adAccountId` on each row are equal. One login can cover several: an agency managing multiple clients has one per client. Reading this does NOT switch account: Apple Ads is pinned to the ONE chosen when the connection was made, so an agent can never act as another client’s. To use a different one, pick it in Settings > Connectors > Apple Ads > Manage accounts, with set_connector_accounts, or by reconnecting. A payment model of null is worth reporting: Apple states that without one, campaigns cannot run. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}The App Store product pages this ad account can advertise — the post-tap destination a creative points at. Every app has exactly one DEFAULT product page (its standard listing, needing no setup) plus any CUSTOM PRODUCT PAGES the developer built in App Store Connect, each with its own productPageId (a UUID, not an integer) that a CUSTOM_PRODUCT_PAGE creative requires. Filter by adamId. Pass localeDetails:true to read the per-language content instead — appName, subtitle, promotional text and the screenshots and preview videos by device class — for a custom page (productPageId) or for the app’s default listing (adamId); Hermoso picks the right one of Apple’s two endpoints for you. READ-ONLY BY DESIGN: product pages are authored in App Store Connect, not through any API, and a page you just published appears here only after a short propagation delay. Free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of rows to return |
| state | string | Filter by page state; the typical live value is PUBLISHED. |
| adamId | string | App Store id — one app’s pages, or its DEFAULT listing’s locale details. |
| offset | number | rows to skip, for paging |
| language | string | With localeDetails — a language, e.g. "en". |
| languageCode | string | With localeDetails — one locale, e.g. "en-US". Omit for all. |
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.
| Argument | Type | Description |
|---|---|---|
| type | string | Default daily_budget. |
| limit | number | maximum number of recommendations to return |
| state | string | AVAILABLE (default), APPLIED, DISMISSED, DELETE, or "ALL" to skip the filter. |
| offset | number | recommendations to skip, for paging |
| promotedObjectId* | string | REQUIRED — the app’s adamId, or an Apple Maps brandId. |
| promotedObjectType |
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.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of markets to return |
| offset | number | markets to skip, for paging |
| filters | array | Scope to specific markets, e.g. [{"field":"countryCode","operator":"IN","value":["US","DE"]}]. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of markets to return"
},
"offset": {
"type": "number",
"description": "markets to skip, for paging"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "Scope to specific markets, e.g. [{\"field\":\"countryCode\",\"operator\":\"IN\",\"value\":[\"US\",\"DE\"]}]."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number of the listing |
| size | number | rows per page |
| assetIds | array | only these asset ids |
| resourceType | string | asset kind to list: image, video or html |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number of the listing"
},
"size": {
"type": "number",
"description": "rows per page"
},
"assetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "only these asset ids"
},
"resourceType": {
"enum": [
"image",
"video",
"html"
],
"type": "string",
"description": "asset kind to list: image, video or html"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | from 1 |
| size | number | 1–100, default 100 |
| campaignIds | array | only these ids (up to 100) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "from 1"
},
"size": {
"type": "number",
"description": "1–100, default 100"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "only these ids (up to 100)"
}
}
}The AppLovin account’s product catalogs and their variant sets: the catalogId and variantSetId a dynamic-ads campaign (create_applovin_ads_campaign) needs. Website accounts. Read-only, free. Needs the Campaign Management key, which exists once AppLovin enables API access on the account; without it the answer says how to get it.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number of the listing |
| size | number | rows per page |
| campaignId | string | campaign whose creative sets to list; omit for the whole account |
| creativeSetIds | array | only these creative set ids |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number of the listing"
},
"size": {
"type": "number",
"description": "rows per page"
},
"campaignId": {
"type": "string",
"description": "campaign whose creative sets to list; omit for the whole account"
},
"creativeSetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "only these creative set ids"
}
}
}The URLs fetched on demand as Bingbot for this site, or one of them in detail (pass `url`) including what Bingbot actually received. This is how you see a page through the CRAWLER's eyes rather than a browser's — a robots block, a redirect chain or a JS-only page all look fine in a browser and wrong here. An empty list means nothing has been fetched on demand yet, which is a real answer and not a failure. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| url | string | optional — one full URL, for the stored detail of that fetch |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"url": {
"type": "string",
"description": "optional — one full URL, for the stored detail of that fetch"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}The sitemaps registered for this site — Bing calls them FEEDS — with the status, URL count, file size and last-crawled date Bing holds for each. This is the read to make BEFORE submitting or removing one, because both of those are addressed by the sitemap's full URL exactly as Bing stores it. Pass `feedUrl` to expand a sitemap INDEX into its child sitemaps (Microsoft's GetFeedDetails); an empty answer for a plain non-index sitemap is a real answer, not a failure. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| feedUrl | string | optional — the full URL of a sitemap INDEX, to list its children |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"feedUrl": {
"type": "string",
"description": "optional — the full URL of a sitemap INDEX, to list its children"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}The Bing Webmaster Tools sites SHARED WITH THIS PROFILE, each exactly as Bing holds it (e.g. "https://example.com"). CALL THIS FIRST — every other Bing tool takes that exact string. THIS IS NOT EVERY SITE THE CONNECTION CAN REACH: Microsoft issues one key per USER covering every site that Bing account verified, so the user ticks which belong to THIS profile and any other one is refused by name. An empty list means nothing is ticked yet — point the user at Settings > Connectors > Bing Webmaster Tools > Manage accounts (or call set_connector_accounts with provider "bing_webmaster") and never name or guess a site. A site flagged not verified will refuse every later call. Read-only, 0 credits.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}List every profile on this account (id + name; a profile is a brand, a creator or a personal workspace) and which one this connection acts on, PLUS any profile another account shared with you. Call this, then use_brand to switch; a SHARED profile switches the same way (pass its name or the profile id printed here). If a profile looks empty when the app shows it full, you are acting on a different one: call this first. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Show every identity a connected account can act as, and which ones this PROFILE is currently allowed to use — Facebook Pages + Instagram + Meta ad accounts, Google Ads customers, LinkedIn company Pages (and the personal profile), Pinterest ad accounts, Microsoft Advertising accounts. One person often administers several; only the ticked ones can be posted to or spent from. Call this before set_connector_accounts, and let the USER pick — never guess. Providers: tiktok, x, youtube, threads, bluesky, telegram, pinterest, instagram, meta, google_ads, linkedin, pinterest_ads, linkedin_ads, reddit_ads, apple_ads, microsoft_ads, google_business, google_analytics, snapchat_ads, x_ads, tiktok_ads, google_tag_manager, google_search_console, bing_webmaster. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| provider* | string | which connector’s accounts to list |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"provider": {
"enum": [
"tiktok",
"x",
"youtube",
"threads",
"bluesky",
"telegram",
"pinterest",
"instagram",
"meta",
"google_ads",
"linkedin",
"pinterest_ads",
"linkedin_ads",
"reddit_ads",
"apple_ads",
"microsoft_ads",
"google_business",
"google_analytics",
"snapchat_ads",
"x_ads",
"tiktok_ads",
"google_tag_manager",
"google_search_console",
"bing_webmaster"
],
"type": "string",
"description": "which connector’s accounts to list"
}
}
}List the third-party accounts connected to this workspace (Meta, Google Ads, Google Drive/Sheets/Docs, YouTube, LinkedIn, OneDrive, Slack, …) — provider, status and the connected account label — PLUS which providers are available to connect. IT ALSO FLAGS A CONNECTION WHOSE PERMISSIONS ARE OUT OF DATE: a provider writes its granted permission set into the token at consent time, so a connection authorized before a permission was approved does not carry it and never will — those calls are refused by the provider and no retry or wait can change it. Check this FIRST when a connected provider starts refusing things. The fix is a browser: the user reconnects under Workspace > Connectors. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}List this workspace’s SAVED CREATORS — the reusable on-camera cast (AI creators made here, a person pulled from a social profile, a consented photo upload). Read-only, FREE. Each entry gives the name, the PORTRAIT URL, where the portrait came from and whether a real person’s likeness consent is on file, how many extra pose plates exist, and any chosen or cloned voice. That casts them for the whole spot (and skips the character-portrait render, so it costs less than not casting anyone). THE PORTRAIT URL IS THE REUSE HANDLE for the raw lanes. CALL THIS BEFORE OFFERING TO GENERATE A NEW PERSON: re-casting somebody the workspace already has keeps the SAME face across every ad, while a fresh person costs credits and breaks that continuity. An empty answer means the workspace genuinely has no cast yet.
| Argument | Type | Description |
|---|---|---|
| limit | number | max creators to return (default 24) |
| gender | string | filter the PRESET creators by gender (the saved cast is never filtered) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max creators to return (default 24)"
},
"gender": {
"enum": [
"female",
"male"
],
"type": "string",
"description": "filter the PRESET creators by gender (the saved cast is never filtered)"
}
}
}List the Google Drive files & folders Hermoso can reach: the ones it created plus any the user picked with the Google file picker in the app (drive.file, never their whole Drive). This is how you find a picked file's id. Filter by query (name contains …), folderId, or onlyFolders:true; paginate with pageToken. Read-only. A FOLDER: picking a folder hands over the folder alone, not its files, so a folder can list empty or partly; the reply then carries `pickUrl`, an app link that opens the picker inside that folder (Cmd+A / Ctrl+A selects every file and subfolder). Give the user that exact link, then list again. recursive:true walks every subfolder Hermoso can see (each row has its `path`) and reports picked files whose folder it cannot see.
| Argument | Type | Description |
|---|---|---|
| query | string | only files whose name contains this |
| folderId | string | list the contents of this folder id |
| pageSize | number | rows per page (1–200, default 50) |
| pageToken | string | cursor from a previous call |
| recursive | boolean | with folderId: the whole tree |
| onlyFolders | boolean | list folders only |
| includeTrashed | boolean |
The errors actually recorded against this workspace, GROUPED by fingerprint — the same failure at the same call site is one row with a hit count and first/last seen, sorted defects-first. Each row says whose side it is: `ours` (a defect worth fixing), `user` (a refusal we deliberately authored, e.g. not-connected or out-of-credits), or `unknown` (a vendor 4xx we cannot attribute — never guessed). Free text, tokens, emails and creative are redacted before anything is stored, so an input echo shows shapes and lengths, not content. Filter by surface (http/mcp/agent/job/client) or kind. Read-only, 0 credits. Scoped to your own workspace; an operator whose client carries the admin key sees every account.
| Argument | Type | Description |
|---|---|---|
| kind | string | 'ours' = a defect; 'user' = a refusal we authored; 'unknown' = we could not tell |
| limit | number | how many groups to return (default 50, max 200) |
| since | string | ISO timestamp — only groups last seen at or after this |
| surface | string | where it happened: http (an API route), mcp (an agent tool), agent (the in-app Studio agent), job (an async render/publish), client (a browser crash) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"ours",
"user",
"unknown"
],
"type": "string",
"description": "'ours' = a defect; 'user' = a refusal we authored; 'unknown' = we could not tell"
},
"limit": {
"type": "number",
"description": "how many groups to return (default 50, max 200)"
},
"since": {
"type": "string",
"description": "ISO timestamp — only groups last seen at or after this"
},
"surface": {
"enum": [
"http",
"mcp",
"agent",
"job",
"client"
],
"type": "string",
"description": "where it happened: http (an API route), mcp (an agent tool), agent (the in-app Studio agent), job (an async render/publish), client (a browser crash)"
}
}
}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).
| Argument | Type | Description |
|---|---|---|
| limit | number | max campaigns (1–500, default 100) |
| since | string | start date YYYY-MM-DD (with until) |
| until | string | end date YYYY-MM-DD |
| status | string | filter campaigns by status |
| metrics | boolean | include performance metrics (default true) |
| customerId | string | 10-digit account id (dashes ok) — omit to list accessible accounts |
| datePreset | string | metrics window preset (default LAST_30_DAYS) |
| loginCustomerId |
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.
| Argument | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"includeRemoved": {
"type": "boolean",
"description": "also list REMOVED conversion actions"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}COMPANIES (accounts) from the brand's OWN HubSpot CRM, newest first: name, domain, website, LinkedIn company page, industry, employees, annual revenue, lifecycle stage, lead status, country/city, owner (rep name), original source + drill-downs, associated contacts and deals, total revenue — plus a de-duplicated `domains` list, which is what LinkedIn company targeting and account-based (ABM) audiences are built from. search matches name / domain / website / phone; filters lifecycleStage (e.g. customer, opportunity, salesqualifiedlead), industry (HubSpot's internal value, e.g. COMPUTER_SOFTWARE), hubspotOwnerId, createdSince (YYYY-MM-DD). Read-only, free. Needs HubSpot connected.
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor from the previous page |
| limit | number | 1–100, default 25 |
| search | string | name, domain, website or phone |
| ownerId | string | older name for hubspotOwnerId (MCP only) |
| industry | string | HubSpot's internal industry value, e.g. COMPUTER_SOFTWARE |
| createdSince | string | YYYY-MM-DD |
| hubspotOwnerId | string | owner id from list_hubspot_owners |
Contacts from the brand's OWN HubSpot CRM, newest first — email, name, lifecycle stage, lead status, created date, original source — or a search by email / first name / last name (whole-token match, wildcards allowed). Read-only, free. Needs HubSpot connected (Settings > Connectors > HubSpot).
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor from the previous page |
| limit | number | 1–100, default 25 |
| search | string | email or name to search for |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor from the previous page"
},
"limit": {
"type": "number",
"description": "1–100, default 25"
},
"search": {
"type": "string",
"description": "email or name to search for"
}
}
}The sales reps (OWNERS) in the brand's HubSpot: id, name, email, teams. An owner's id is what create_hubspot_contact's hubspotOwnerId assigns a lead to, and what list_hubspot_companies filters by. search narrows by name or email; archived:true lists DEACTIVATED owners instead. Read-only, free. Needs HubSpot connected.
| Argument | Type | Description |
|---|---|---|
| search | string | name or email contains |
| archived | boolean | true = deactivated owners only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"search": {
"type": "string",
"description": "name or email contains"
},
"archived": {
"type": "boolean",
"description": "true = deactivated owners only"
}
}
}List the most recent render jobs + how many are currently running, so you can report on or resume in-flight work.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Browse this workspace's Library — every image/video generated in the Studio, and uploads (source:'upload'), newest first (the same Library the web app shows). Returns served URLs you can open directly or hand to fetch_asset for a download link, plus each asset's id, kind, model, and age. Free, read-only.
| Argument | Type | Description |
|---|---|---|
| kind | string | filter by asset kind (default 'all') |
| limit | number | max assets to return (default 20, max 60) |
| uploads | boolean |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"image",
"video",
"document",
"all"
],
"type": "string",
"description": "filter by asset kind (default 'all')"
},
"limit": {
"type": "number",
"description": "max assets to return (default 20, max 60)"
},
"uploads": {
"type": "boolean"
}
}
}Read the LinkedIn ad accounts this connection can reach, and — with adAccountId — that account’s campaign groups and campaigns: name, status, objective, budgets, and LinkedIn’s own servingStatuses, which explain WHY something is not delivering (billing hold, start-date hold, parent-status hold). LinkedIn’s Advertising API is an approval-gated product, and on its Development tier each ad account must ALSO be mapped to the app in LinkedIn’s Developer Portal — so if nothing is reachable, say that rather than implying the user has no ad account. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| statuses | array | filter campaigns by ACTIVE, PAUSED, ARCHIVED or DRAFT |
| campaignId | string | also return the CREATIVES (the actual ads) under this campaign, each with its intendedStatus, whether it isServing, and LinkedIn’s own servingHoldReasons |
| adAccountId | string | LinkedIn ad account id — omit to list the reachable accounts |
| campaignIds | array | same, for several campaigns at once |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"statuses": {
"type": "array",
"items": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED",
"DRAFT"
],
"type": "string"
},
"description": "filter campaigns by ACTIVE, PAUSED, ARCHIVED or DRAFT"
},
"campaignId": {
"type": "string",
"description": "also return the CREATIVES (the actual ads) under this campaign, each with its intendedStatus, whether it isServing, and LinkedIn’s own servingHoldReasons"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id — omit to list the reachable accounts"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "same, for several campaigns at once"
}
}
}List the conversion rules on a LinkedIn ad account, including ones SHARED from other accounts in the same Business Manager. Use it to find the conversionId send_linkedin_conversions needs, and to check whether a rule can actually receive API events — only a rule whose conversionMethod is CONVERSIONS_API and which is enabled can, and a rule built for the Insight Tag cannot. Zero rules genuinely means none exist; say that rather than implying a failure. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
}
}
}List the durable facts & preferences saved in this workspace’s Memory (what the studio remembers about the brand, audience, taste, and do/don’t rules) — the same Memory the web app shows. These shape every future ad. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max items (default 50, max 200) |
| category | string | filter to one bucket (Brand/Audience/Taste/Do/Don’t/Preference) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max items (default 50, max 200)"
},
"category": {
"type": "string",
"description": "filter to one bucket (Brand/Audience/Taste/Do/Don’t/Preference)"
}
}
}List the Google Merchant Center accounts this brand's connected Google account can reach. EVERY other Merchant tool needs the merchantCenterId this returns, and an agency often has several, so never guess one — ask the user which store. Read-only and free. If it is refused, the Merchant Center permission has not been granted yet; that is not a broken connection and reconnecting will not change it.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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.
| Argument | Type | Description |
|---|---|---|
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}The per-store and per-region price and availability overrides on one Shopping product. An override REPLACES the product's own price and availability for that store or region; where there is no override the product-level values apply, so an empty result is the normal answer and does NOT mean the product is missing. Needs the product's offerId plus its contentLanguage and feedLabel, because in the Merchant API those three together ARE the product's address rather than optional filters. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| kind | string | omit to read both |
| offerId* | string | the product's own id in the feed, from list_merchant_products |
| feedLabel* | string | usually the feed target country, e.g. "US". Part of the product's address, not a filter |
| contentLanguage* | string | two-letter feed language, e.g. "en". Part of the product's address, not a filter |
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"offerId",
"contentLanguage",
"feedLabel"
],
"properties": {
"kind": {
"enum": [
"local",
"regional"
],
"type": "string",
"description": "omit to read both"
},
"offerId": {
"type": "string",
"description": "the product's own id in the feed, from list_merchant_products"
},
"feedLabel": {
"type": "string",
"description": "usually the feed target country, e.g. \"US\". Part of the product's address, not a filter"
},
"contentLanguage": {
"type": "string",
"description": "two-letter feed language, e.g. \"en\". Part of the product's address, not a filter"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}Read the account-level issues Google reports on a Merchant Center — the answer to "why is this product not showing?". Needs merchantCenterId from list_merchant_accounts. Read-only and free.
| Argument | Type | Description |
|---|---|---|
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}Read the products in a Merchant Center feed — what the merchant actually offers, which is what Shopping and retail Performance Max campaigns serve. Needs merchantCenterId from list_merchant_accounts. Read-only and free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max products (default 50, max 250) |
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"limit": {
"type": "number",
"description": "max products (default 50, max 250)"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}The sale and discount promotions on a Merchant Center account, with each one's PER-DESTINATION status and any issues Google has raised against it. GOOGLE VALIDATES PROMOTIONS ASYNCHRONOUSLY, so 'created' and 'running' are different states and only destinationStatuses tells them apart — never report a sale as live off a successful create. Read-only, 0 credits. Needs Google Ads connected (Merchant Center rides the same connection).
| Argument | Type | Description |
|---|---|---|
| limit | number | up to 250, default 50 |
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"limit": {
"type": "number",
"description": "up to 250, default 50"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}Read the EXISTING campaigns, ad sets, or ads on a connected Meta ad account — id, name, status, budget, objective, and at level ad WHY Meta rejected or flagged an ad (its review feedback, in Meta’s words). Pass adAccountId and level (campaign|adset|ad). Scope to a parent with campaignId (-> its ad sets/ads) or adsetId (-> its ads), and filter by status (ACTIVE/PAUSED/…). Read-only — use it to inspect an account before editing/deleting, or to answer "what’s running?".
| Argument | Type | Description |
|---|---|---|
| level | string | what to list (default campaign) |
| limit | number | max rows (1–200, default 50) |
| status | string | filter by effective status, e.g. ACTIVE / PAUSED |
| adsetId | string | list the ads under this ad set |
| campaignId | string | list the ad sets / ads under this campaign |
| adAccountId* | string | Ad account id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"level": {
"enum": [
"campaign",
"adset",
"ad"
],
"type": "string",
"description": "what to list (default campaign)"
},
"limit": {
"type": "number",
"description": "max rows (1–200, default 50)"
},
"status": {
"type": "string",
"description": "filter by effective status, e.g. ACTIVE / PAUSED"
},
"adsetId": {
"type": "string",
"description": "list the ads under this ad set"
},
"campaignId": {
"type": "string",
"description": "list the ad sets / ads under this campaign"
},
"adAccountId": {
"type": "string",
"description": "Ad account id"
}
}
}List the custom audiences and lookalikes on a connected Meta ad account — id, name, type, approximate size, and whether Meta says it is ready to target. Call it before create_meta_audience (so you never build a duplicate) and before targeting one: the ids go straight into create_meta_ad’s targeting.customAudiences / excludedCustomAudiences. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max rows (1–200, default 50) |
| adAccountId* | string | ad account id (act_… or digits) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"limit": {
"type": "number",
"description": "max rows (1–200, default 50)"
},
"adAccountId": {
"type": "string",
"description": "ad account id (act_… or digits)"
}
}
}List the INSTANT LEAD FORMS on a connected Facebook Page — id, name, status, how many leads each has collected and what each one asks. This is where the formId every other lead tool needs comes from, and calling it before create_meta_lead_form is how you avoid building a duplicate. Read-only, free. NEEDS the pages_manage_ads permission, which Hermoso’s Meta consent screen asks for. If Meta answers "Requires pages_manage_ads", the user must RECONNECT Meta under Settings > Connectors > Meta — a connection made before that permission was added cannot gain it by retrying. Call the tool rather than pre-refusing: any refusal comes from Meta and names the one thing that fixes it.
| Argument | Type | Description |
|---|---|---|
| limit | number | max rows (1–100, default 25) |
| pageId | string | Which connected Page — required only if the profile has more than one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max rows (1–100, default 25)"
},
"pageId": {
"type": "string",
"description": "Which connected Page — required only if the profile has more than one"
}
}
}List the META PIXELS on one of the brand’s ad accounts — id, name, when it was created, and WHEN IT LAST FIRED. This is where the pixelId every conversion tool needs comes from: create_meta_ad takes it (with conversionEvent) to optimise an ad set for OFFSITE_CONVERSIONS instead of link clicks, and create_meta_audience needs it to build a website retargeting audience. Without this tool that id could only be read off a screen in Events Manager. READ lastFiredAt BEFORE YOU TRUST A PIXEL: one that has NEVER FIRED is not installed on the site, so an ad optimising against it will spend and never learn. Pass includeCode:true to get the <script> snippet for installation (it is long, so it is off by default). Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adAccountId* | string | The ad account, with or without the act_ prefix |
| includeCode | boolean | also return the full pixel <script> snippet — only ask when the user is about to install it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"adAccountId": {
"type": "string",
"description": "The ad account, with or without the act_ prefix"
},
"includeCode": {
"type": "boolean",
"description": "also return the full pixel <script> snippet — only ask when the user is about to install it"
}
}
}List the playables uploaded to a Meta ad account (id and name), for create_meta_ad's playableAssetId. Free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of playables to return |
| adAccountId* | string | act_… or the bare number |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"limit": {
"type": "number",
"description": "maximum number of playables to return"
},
"adAccountId": {
"type": "string",
"description": "act_… or the bare number"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId | string | ad group whose audience targeting to list; pass this or campaignId |
| campaignId | string | campaign whose audience targeting to list; pass this or adGroupId |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "ad group whose audience targeting to list; pass this or campaignId"
},
"campaignId": {
"type": "string",
"description": "campaign whose audience targeting to list; pass this or adGroupId"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| types | array | audience types, default CustomerList |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| audienceIds | array | up to 100 specific ids — omit for all of the requested type |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"types": {
"type": "array",
"items": {
"type": "string"
},
"description": "audience types, default CustomerList"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"audienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 100 specific ids — omit for all of the requested type"
}
}
}Read the brand’s connected Microsoft Advertising (Bing Ads) account(s). Call with NO adAccountId to list the accounts shared with this profile — do this first to pick a target. Call WITH adAccountId to list that account’s campaigns (id, name, status, daily budget, campaign type, and whether the budget is SHARED). Microsoft statuses are Active / Paused — never Google’s ENABLED — and Microsoft also sets BudgetPaused, BudgetAndManualPaused and Suspended on its own, so report the status you read rather than assuming a paused campaign was paused by a person. Read-only, free. Needs Microsoft Advertising connected (Settings > Connectors > Microsoft Advertising).
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}The conversion goals on a Microsoft Advertising account — what the account COUNTS as a conversion, and therefore what every conversion-optimised bid strategy on it is optimising toward. Flags which ones are OFFLINE goals: those are the only goals send_microsoft_ads_offline_conversions can post to, and its conversionName must match one of their names EXACTLY, including case and spacing. An account with no goals at all has no conversion tracking — say that plainly rather than reporting an empty list, because it means smart bidding there has nothing to aim at. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| goalIds | array | specific goal ids — omit for all of them |
| goalType | string | filter to one Microsoft ConversionGoalType, e.g. OfflineConversion |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goalIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "specific goal ids — omit for all of them"
},
"goalType": {
"type": "string",
"description": "filter to one Microsoft ConversionGoalType, e.g. OfflineConversion"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| listId | string | a shared negative keyword list |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId | string | ad group whose negative keywords to list |
| campaignId | string | campaign whose negative keywords to list |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"listId": {
"type": "string",
"description": "a shared negative keyword list"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "ad group whose negative keywords to list"
},
"campaignId": {
"type": "string",
"description": "campaign whose negative keywords to list"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| campaignId* | string | Microsoft campaign id to read LinkedIn profile targeting from |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "Microsoft campaign id to read LinkedIn profile targeting from"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| tagIds | array | only these UET tag ids; omit for all tags |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"tagIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "only these UET tag ids; omit for all tags"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| 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 |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"merchantId": {
"type": "string",
"description": "Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | up to 250, default 25 |
| accountId | string | older name for adAccountId (MCP only) |
| pageToken | string | nextPageToken from a previous result, to get the next page |
| merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "up to 250, default 25"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"pageToken": {
"type": "string",
"description": "nextPageToken from a previous result, to get the next page"
},
"merchantId": {
"type": "string",
"description": "Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | up to 250, default 25 |
| accountId | string | older name for adAccountId (MCP only) |
| pageToken | string | the nextPageToken from a previous call |
| productId | string | fully qualified id (channel:contentLanguage:targetCountry:offerId, e.g. Online:en:US:Sku123) to fetch ONE offer |
| merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "up to 250, default 25"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"pageToken": {
"type": "string",
"description": "the nextPageToken from a previous call"
},
"productId": {
"type": "string",
"description": "fully qualified id (channel:contentLanguage:targetCountry:offerId, e.g. Online:en:US:Sku123) to fetch ONE offer"
},
"merchantId": {
"type": "string",
"description": "Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| toDate | string | YYYY-MM-DD, inclusive |
| fromDate | string | YYYY-MM-DD, inclusive |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"toDate": {
"type": "string",
"description": "YYYY-MM-DD, inclusive"
},
"fromDate": {
"type": "string",
"description": "YYYY-MM-DD, inclusive"
}
}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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.
| Argument | Type | Description |
|---|---|---|
| type | string | count type for the event amounts: general (default), unique or average |
| event | string | pass an event name to list ITS properties instead of listing events |
| limit | number | Mixpanel own defaults are 255 for the vocabulary, 100 for today and 10 for properties |
| window | string | "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 |
Find the Notion pages and databases the user shared with Hermoso, by title, newest edited first. Gives the page id that save_to_notion, append_to_notion_page and read_notion_page take, or the database id to save rows into. Read-only, free. Needs Notion connected (Settings > Connectors > Notion).
| Argument | Type | Description |
|---|---|---|
| type | string | limit results to pages or to databases; omit for both |
| limit | number | 1-100, default 25 |
| query | string | words in the title |
| cursor | string | from the previous page |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"type": {
"enum": [
"page",
"database"
],
"type": "string",
"description": "limit results to pages or to databases; omit for both"
},
"limit": {
"type": "number",
"description": "1-100, default 25"
},
"query": {
"type": "string",
"description": "words in the title"
},
"cursor": {
"type": "string",
"description": "from the previous page"
}
}
}List files & folders in the user’s OneDrive — the root by default, a folder’s contents (folderId), or a name search (query). onlyFolders:true lists folders only. Paginate with pageToken (the cursor from a previous call). Read-only.
| Argument | Type | Description |
|---|---|---|
| query | string | search — only items whose name matches this |
| folderId | string | list the contents of this folder id |
| pageSize | number | rows per page (1–200, default 50) |
| pageToken | string | cursor from a previous call |
| onlyFolders | boolean | list folders only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"query": {
"type": "string",
"description": "search — only items whose name matches this"
},
"folderId": {
"type": "string",
"description": "list the contents of this folder id"
},
"pageSize": {
"type": "number",
"description": "rows per page (1–200, default 50)"
},
"pageToken": {
"type": "string",
"description": "cursor from a previous call"
},
"onlyFolders": {
"type": "boolean",
"description": "list folders 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.
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-100, default 25 |
| cursor | string | nextCursor from the previous page |
| audienceId* | string | ChatGPT Ads audience id whose operations to list |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"limit": {
"type": "number",
"description": "1-100, default 25"
},
"cursor": {
"type": "string",
"description": "nextCursor from the previous page"
},
"audienceId": {
"type": "string",
"description": "ChatGPT Ads audience id whose operations to list"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | max audiences to return |
| intendedUse | string | only audiences eligible for this use |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max audiences to return"
},
"intendedUse": {
"enum": [
"exclusion",
"inclusion",
"bid_multiplier"
],
"type": "string",
"description": "only audiences eligible for this use"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId | string | only changes to this ad |
| after | string | pagination cursor for the next page |
| limit | number | 1-100, default 25 |
| order | string | asc or desc, by time |
| since | string | start of the window: date, ISO time or unix seconds |
| until | string | end of the window: date, ISO time or unix seconds |
| before | string | pagination cursor for the previous page |
| actorId | string | only changes made by this actor id |
| adGroupId | string |
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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}Read the brand’s connected ChatGPT Ads account — the ads that appear below ChatGPT answers. Every campaign, ad group and ad row carries servingIssues, OpenAI’s own list of what is blocking delivery (payment method, brand review, budget spent, ad in review, landing page not crawlable, country policy…) with a plain meaning each: null means OpenAI was not asked, [] means it reports no blocker, which is still not a promise of impressions. Call with NO ids to get the ad account itself (its three names: brandName shown in ads, accountName internal to Ads Manager, legalName; currency, status, brand review and business review state) plus its campaigns; with campaignId to list that campaign’s ad groups; with adGroupId to list that ad group’s ads, including each ad’s REVIEW status, which is what decides whether it can ever show. Statuses here are active / paused / archived. Read-only, free, zero spend risk. Needs ChatGPT Ads connected under Settings > Connectors > ChatGPT Ads in the Hermoso app; one connection covers one ad account.
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor from a previous page |
| limit | number | page size, default 100 |
| adGroupId | string | list this ad group’s ads |
| campaignId | string | list this campaign’s ad groups |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor from a previous page"
},
"limit": {
"type": "number",
"description": "page size, default 100"
},
"adGroupId": {
"type": "string",
"description": "list this ad group’s ads"
},
"campaignId": {
"type": "string",
"description": "list this campaign’s ad groups"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | max conversion events to return |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max conversion events to return"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | max uploads to return |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max uploads to return"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor from the previous page |
| limit | number | max feeds to return |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor from the previous page"
},
"limit": {
"type": "number",
"description": "max feeds to return"
}
}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | default 100, max 1000 |
| since | string | ISO date |
| formId | string | return only leads from this lead form id |
| includeSynthetic | boolean | true = include synthetic test leads, which are hidden by default |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "default 100, max 1000"
},
"since": {
"type": "string",
"description": "ISO date"
},
"formId": {
"type": "string",
"description": "return only leads from this lead form id"
},
"includeSynthetic": {
"type": "boolean",
"description": "true = include synthetic test leads, which are hidden by default"
}
}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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.
| Argument | Type | Description |
|---|---|---|
| what | string | audiences (default) or customer_lists |
| ownership | string | OWNED or RECEIVED; RECEIVED lists audiences shared with this account |
| audienceId | string | read one audience in full by its id |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| customerListId | string | read one customer list in full by its id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"what": {
"enum": [
"audiences",
"customer_lists"
],
"type": "string",
"description": "audiences (default) or customer_lists"
},
"ownership": {
"enum": [
"OWNED",
"RECEIVED"
],
"type": "string",
"description": "OWNED or RECEIVED; RECEIVED lists audiences shared with this account"
},
"audienceId": {
"type": "string",
"description": "read one audience in full by its id"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"customerListId": {
"type": "string",
"description": "read one customer list in full by its id"
}
}
}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).
| Argument | Type | Description |
|---|---|---|
| statuses | array | filter by ACTIVE, PAUSED, ARCHIVED or DRAFT; omit to include all four |
| campaignId | string | one campaign -> its ad groups and ads too (the only way to enumerate them) |
| adAccountId | string | Pinterest ad account id — omit to list the ad accounts shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"statuses": {
"type": "array",
"items": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED",
"DRAFT"
],
"type": "string"
},
"description": "filter by ACTIVE, PAUSED, ARCHIVED or DRAFT; omit to include all four"
},
"campaignId": {
"type": "string",
"description": "one campaign -> its ad groups and ads too (the only way to enumerate them)"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to list the ad accounts shared with this profile"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| view | string | all (default) lists tags; ocpm_eligible lists (tag, event) goals; page_visit lists page-visit events |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| includeDeleted | boolean | true also includes deleted tags in the list |
| conversionTagId | string | read one tag in full — code snippet and enhanced-match config |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"view": {
"enum": [
"all",
"ocpm_eligible",
"page_visit"
],
"type": "string",
"description": "all (default) lists tags; ocpm_eligible lists (tag, event) goals; page_visit lists page-visit events"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"includeDeleted": {
"type": "boolean",
"description": "true also includes deleted tags in the list"
},
"conversionTagId": {
"type": "string",
"description": "read one tag in full — code snippet and enhanced-match config"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| bookmark | string | the next page, from the last reply |
| adGroupId | string | the ad group |
| campaignId | string | the campaign |
| matchTypes | array | only these match types |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bookmark": {
"type": "string",
"description": "the next page, from the last reply"
},
"adGroupId": {
"type": "string",
"description": "the ad group"
},
"campaignId": {
"type": "string",
"description": "the campaign"
},
"matchTypes": {
"type": "array",
"items": {
"enum": [
"BROAD",
"PHRASE",
"EXACT",
"EXACT_NEGATIVE",
"PHRASE_NEGATIVE"
],
"type": "string"
},
"description": "only these match types"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adGroupId* | string | Shopping ad group whose product-group promotions to list |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId"
],
"properties": {
"adGroupId": {
"type": "string",
"description": "Shopping ad group whose product-group promotions to list"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
}
}
}List the PLAYBOOKS saved in this workspace — the reusable strategy cards (winning hooks, angles, formats and the concrete plays to run) kept from teardowns, angle mining and creatives worth repeating. The same Playbooks the web app's Playbooks tab lists. Read one before planning an ad so you re-run what already worked instead of starting cold. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| full | boolean | true to return every hook/angle/play in the text, not just the headline counts |
| limit | number | max playbooks to return (default 25, max 100) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"full": {
"type": "boolean",
"description": "true to return every hook/angle/play in the text, not just the headline counts"
},
"limit": {
"type": "number",
"description": "max playbooks to return (default 25, max 100)"
}
}
}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.
| Argument | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1–100, default 25"
},
"offset": {
"type": "number",
"description": "for the next page — this tool does not paginate for you"
},
"search": {
"type": "string",
"description": "free-text search over annotation content"
}
}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | 1–100, default 20 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1–100, default 20"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-100, default 25 |
| dateTo | string | stats window end, ISO |
| offset | number | skip this many surveys; use the nextOffset from the previous reply |
| search | string | match survey names containing this text |
| status | string | only surveys in this state |
| archived | boolean | PostHog archived filter: true or false; omit to leave it unfiltered |
| dateFrom | string | stats window start, ISO |
List the product photos ALREADY saved in your workspace — the brand's product library plus any app-store screens (also surfaces photos locked in your OTHER creations, since a set product lands in the shared library). FREE — returns each photo's url + the product it shows (the shop's own title when one is on file). These are already saved: never ask the user to upload them again. Reads YOUR saved brand (pass brandId to target a specific profile — that switches this key's active profile like use_brand).
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands whose product library to list; omit to use the active profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands whose product library to list; omit to use the active profile"
}
}
}The brand's product catalog: each product's id, title, page link, price (and whether it is current), photos, its own selling points and audience, and when it was last checked against its live page. Free. `refresh:'stale'` re-reads rows older than a day first, `refresh:'all'` re-reads every page.
| Argument | Type | Description |
|---|---|---|
| brandId | string | a profile id/name from list_brands; omit for the active profile |
| refresh | string | re-read product pages before listing: 'stale' (older than a day) or 'all' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brandId": {
"type": "string",
"description": "a profile id/name from list_brands; omit for the active profile"
},
"refresh": {
"enum": [
"stale",
"all"
],
"type": "string",
"description": "re-read product pages before listing: 'stale' (older than a day) or 'all'"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name | string | filter by asset name |
| limit | number | 1–100, default 25 |
| types | array | Reddit lists media only |
| 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 |
| pageToken | string | nextPageToken from the previous page |
| 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 |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | filter by name |
| limit | number | default 50, max 100 |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "filter by name"
},
"limit": {
"type": "number",
"description": "default 50, max 100"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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).
| Argument | Type | Description |
|---|---|---|
| adAccountId | string | Reddit ad account id (a2_…) — omit to list the ad accounts shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_…) — omit to list the ad accounts shared with this profile"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| catalogId | string | a catalog id from the list — returns its feeds, product sets and recent imports |
| adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"catalogId": {
"type": "string",
"description": "a catalog id from the list — returns its feeds, product sets and recent imports"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_…) — omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| mode | string | ALL (default), ACTIVE, INACTIVE, UPCOMING or SELECTABLE |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"mode": {
"enum": [
"ALL",
"ACTIVE",
"INACTIVE",
"UPCOMING",
"SELECTABLE"
],
"type": "string",
"description": "ALL (default), ACTIVE, INACTIVE, UPCOMING or SELECTABLE"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | default 50, max 100 |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "default 50, max 100"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_…) — omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| type | string | TEXT, IMAGE, VIDEO or CAROUSEL; omit for all types |
| limit | number | page size, 1-100, default 25 |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"redditProfileId"
],
"properties": {
"type": {
"enum": [
"TEXT",
"IMAGE",
"VIDEO",
"CAROUSEL"
],
"type": "string",
"description": "TEXT, IMAGE, VIDEO or CAROUSEL; omit for all types"
},
"limit": {
"type": "number",
"description": "page size, 1-100, default 25"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"redditProfileId": {
"type": "string",
"description": "the Reddit profile id (t2_…) from list_reddit_ads_profiles"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| ids | array | specific product ids to read |
| limit | number | page size, default 50, max 1000 |
| search | string | text to match against product fields |
| catalogId | string | one of catalogId or productSetId |
| pageToken | string | nextPageToken from the previous page |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| productSetId | string | list the products in this product set instead of the whole catalog |
| approvalDecision | string |
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.
| Argument | Type | Description |
|---|---|---|
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | default 50, max 100 |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "default 50, max 100"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}The sitemaps registered on a Search Console property: when each was last submitted and last downloaded by Google, whether Google has processed it yet, its error and warning counts, and per content type how many URLs it SUBMITTED against how many are INDEXED. That last pair is the fastest read on whether a site has an indexing problem rather than a ranking one. Pass `feedpath` for one sitemap instead of the list, or `sitemapIndex` to list the sitemaps inside an index file. An empty list is a real answer — the property has no sitemap registered — and is not a failure. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| siteUrl* | string | the exact property string from list_search_console_sites |
| feedpath | string | the FULL url of one sitemap, e.g. "https://example.com/sitemap.xml" |
| sitemapIndex | string | the FULL url of a sitemap INDEX, to list the sitemaps it contains |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"siteUrl": {
"type": "string",
"description": "the exact property string from list_search_console_sites"
},
"feedpath": {
"type": "string",
"description": "the FULL url of one sitemap, e.g. \"https://example.com/sitemap.xml\""
},
"sitemapIndex": {
"type": "string",
"description": "the FULL url of a sitemap INDEX, to list the sitemaps it contains"
}
}
}The Google Search Console properties SHARED WITH THIS PROFILE, each with the exact property string every other Search Console tool takes and the connected account's permission level on it. CALL THIS FIRST. A property is EITHER "sc-domain:example.com" (a Domain property, covering every scheme and subdomain) OR the full URL-prefix form "https://example.com/" including scheme and trailing slash — Google treats those as different properties and one of them will 403, so resolve it here rather than guessing, and never pass a bare domain. THIS IS NOT EVERY PROPERTY THE GOOGLE ACCOUNT CAN SEE, deliberately: one login commonly holds a dozen clients' properties, so the user ticks which belong to THIS profile 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 Search Console > Manage accounts (or call list_connector_accounts / set_connector_accounts with provider "google_search_console"); never name or guess a property. A row whose permissionLevel is siteUnverifiedUser will refuse every later call. Read-only, 0 credits. Needs Google Search Console connected.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}The tabs in a Google Spreadsheet, each with its name, numeric sheetId, row/column count and position. Call this BEFORE naming a tab in update_sheet / clear_sheet_range / manage_sheet_tabs / format_sheet, and before proposing to delete one — it is how you learn what the file actually contains instead of guessing at a name. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| sheetUrl | string | a Google Sheets URL — the id is extracted from it |
| spreadsheetId | string | the spreadsheet id (from create_sheet, or list_drive_files for one the user picked) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"sheetUrl": {
"type": "string",
"description": "a Google Sheets URL — the id is extracted from it"
},
"spreadsheetId": {
"type": "string",
"description": "the spreadsheet id (from create_sheet, or list_drive_files for one the user picked)"
}
}
}The marketing activities recorded in the merchant's Shopify > Marketing by Hermoso (and other apps): title, tactic, channel, status, UTM, budget, ad spend, dates and the remoteId each was recorded under. Filter by remoteId or a Shopify search query (e.g. title:summer). Newest first; pass cursor for more. Read-only, free. Needs the store to have approved Hermoso's marketing permission. Only works for accounts created by installing Hermoso from the Shopify App Store.
| Argument | Type | Description |
|---|---|---|
| limit | number | 1–100, default 25 |
| query | string | Shopify search syntax, e.g. title:summer or tactic:AD |
| cursor | string | pageInfo cursor from the previous reply, to fetch the next page |
| remoteId | string | return only the activity recorded under this remoteId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1–100, default 25"
},
"query": {
"type": "string",
"description": "Shopify search syntax, e.g. title:summer or tactic:AD"
},
"cursor": {
"type": "string",
"description": "pageInfo cursor from the previous reply, to fetch the next page"
},
"remoteId": {
"type": "string",
"description": "return only the activity recorded under this remoteId"
}
}
}List the bundled Hermoso SKILLS — multi-step workflow instructions (SKILL.md) that orchestrate the other tools (research an ad space, plan+render a finished ad, product photoshoot, raw generation) — plus the in-app strategy skills and creative recipes. Call get_skill to load a bundle. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}List the Snapchat AD ACCOUNTS SHARED WITH THIS PROFILE — the ones it may actually build on and spend from, which is NOT everything the Snapchat login can reach — id, name, currency, timezone and status. Every other Snapchat Ads tool needs an adAccountId and this is where it comes from. One call returns both tiers, because Snap nests ad accounts inside their organization. An account flagged as a TEST account is marked as such — those cannot serve real ads. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}The custom AUDIENCES on a Snapchat ad account — customer lists, lookalikes, website-event (pixel) and engagement audiences — with approximate size, upload status and whether each can be targeted yet (READY, TOO_FEW_USERS, NOT_READY). Pass audienceId for one. Target them with create_snapchat_ads_ad_squad(audiences / excludeAudiences). Read-only, free.
| Argument | Type | Description |
|---|---|---|
| audienceId | string | one audience id to read; omit to list all |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"audienceId": {
"type": "string",
"description": "one audience id to read; omit to list all"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}Read the whole Snapchat ad tree for an ad account — campaigns, ad squads and ads with their statuses. The three tiers are fetched separately so one failure cannot take the tree down, and a tier that FAILED to read is reported in `partial` rather than as an empty list: an empty list here means an empty account, never a failed read. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adAccountId | string | from list_snapchat_ads_accounts — omit only when exactly one is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adAccountId": {
"type": "string",
"description": "from list_snapchat_ads_accounts — omit only when exactly one is reachable"
}
}
}The LEAD FORMS on a Snapchat ad account — id, name, title and questions — or one form with formId. A Snapchat lead ad opens one of these: build the LEAD_GENERATION creative with upload_snapchat_ads_creative(leadGenerationFormId), then create_snapchat_ads_ad. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| formId | string | read one form |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"formId": {
"type": "string",
"description": "read one form"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}The Snapchat leads Snapchat has delivered to Hermoso for this brand, newest first: leadId, createdAt, email, firstName, lastName, phone, postalCode, address, birthday, jobTitle, companyName, the custom answers and consents, and the form / campaign / ad squad / ad they came from. Only leads that arrived after manage_snapchat_ads_lead_webhook subscribed their form exist — an empty list means none ARRIVED, not that nobody signed up before delivery was on. Snapchat's sample leads (action:'test') are hidden unless includeTest. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | default 100, max 1000 |
| since | string | ISO date |
| formId | string | only leads from this lead form id |
| includeTest | boolean | true includes Snapchat sample leads, which are hidden by default |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "default 100, max 1000"
},
"since": {
"type": "string",
"description": "ISO date"
},
"formId": {
"type": "string",
"description": "only leads from this lead form id"
},
"includeTest": {
"type": "boolean",
"description": "true includes Snapchat sample leads, which are hidden by default"
}
}
}The Snap Pixels on a Snapchat ad account — id, name and status. A conversion ad squad (optimizationGoal PIXEL_PAGE_VIEW, PIXEL_ADD_TO_CART, PIXEL_PURCHASE or PIXEL_SIGNUP) optimises toward a pixel, so call this first and pass the pixelId to create_snapchat_ads_ad_squad. A pixel is made in Snapchat Ads Manager (Events Manager) and installed on the site; Snapchat publishes no API that creates one. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}FIND THE PUBLIC PROFILE ID EVERY SNAPCHAT AD CREATIVE REQUIRES. Snapchat has required profile_properties on every creative since 2024-02-26, so without one upload_snapchat_ads_creative cannot build anything — this is where the id comes from. Snapchat publishes no clean list-profiles endpoint, so this reads the ad account SHARING POLICIES (the documented mechanism by which a profile reaches an ad account) and reports which resource-type token Snapchat accepted. IT CAN LEGITIMATELY FAIL: the Public Profile API is on a different host and documents its own OAuth scope which this connection does not hold, and a failure is reported as "could not tell" with the manual way out — NEVER as "you have no profiles", because an empty list there would look like a real answer. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| resourceType | string | override the shared_resource_types token if Snapchat documents a different one — by default several are tried and the one that works is reported |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"resourceType": {
"type": "string",
"description": "override the shared_resource_types token if Snapchat documents a different one — by default several are tried and the one that works is reported"
}
}
}Recent charges on the brand's OWN Stripe account — amount, currency, status, refunds, customer email, description — newest first, optionally for one customer. Read-only, free. Needs Stripe connected.
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last id of the previous page |
| limit | number | 1–100, default 25 |
| customerId | string | a Stripe customer id (cus_…) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last id of the previous page"
},
"limit": {
"type": "number",
"description": "1–100, default 25"
},
"customerId": {
"type": "string",
"description": "a Stripe customer id (cus_…)"
}
}
}Coupons on the brand's Stripe account: percent or amount off, duration, validity and redemptions. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last coupon id from the previous page |
| limit | number | rows to return, 1 to 100; default 25 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last coupon id from the previous page"
},
"limit": {
"type": "number",
"description": "rows to return, 1 to 100; default 25"
}
}
}Recent customers on the brand's OWN Stripe account — email, name, created, currency, delinquent — newest first, or one customer by exact email. Read-only, free. Needs Stripe connected.
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last id of the previous page |
| string | exact email match | |
| limit | number | 1–100, default 25 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last id of the previous page"
},
"email": {
"type": "string",
"description": "exact email match"
},
"limit": {
"type": "number",
"description": "1–100, default 25"
}
}
}Invoices on the brand's Stripe account, newest first: number, status (draft, open, paid, uncollectible, void), amount due and paid, customer email, dates and the hosted invoice link; filter by status or customer. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last invoice id from the previous page |
| limit | number | rows to return, 1 to 100; default 25 |
| status | string | filter to draft, open, paid, uncollectible or void |
| customerId | string | Stripe customer id, cus_... |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last invoice id from the previous page"
},
"limit": {
"type": "number",
"description": "rows to return, 1 to 100; default 25"
},
"status": {
"type": "string",
"description": "filter to draft, open, paid, uncollectible or void"
},
"customerId": {
"type": "string",
"description": "Stripe customer id, cus_..."
}
}
}Payment links on the brand's Stripe account: URL, active or not, whether the promotion-code box shows, what happens after payment, and their line items (price, amount). Filter active true/false. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last payment link id from the previous page |
| limit | number | rows to return, 1 to 100; default 25 |
| active | boolean | true = active links only; false = inactive links only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last payment link id from the previous page"
},
"limit": {
"type": "number",
"description": "rows to return, 1 to 100; default 25"
},
"active": {
"type": "boolean",
"description": "true = active links only; false = inactive links only"
}
}
}Products on the brand's Stripe account with their active prices (amount, currency, one-time or recurring interval), which is what create_stripe_payment_link and create_stripe_subscription take. active:false lists archived products too. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last product id from the previous page |
| limit | number | rows to return, 1 to 100; default 25 |
| active | boolean | false also lists archived products; omit or true for active products only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last product id from the previous page"
},
"limit": {
"type": "number",
"description": "rows to return, 1 to 100; default 25"
},
"active": {
"type": "boolean",
"description": "false also lists archived products; omit or true for active products only"
}
}
}Promotion codes on the brand's Stripe account — the codes customers TYPE at checkout — with the coupon each applies, active or not, times redeemed / max, expiry, first-purchase-only and minimum-order rules. Filter by active, code or couponId. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| code | string | customer-typable promotion code text to find |
| after | string | pagination cursor: the last promotion code id from the previous page |
| limit | number | rows to return, 1 to 100; default 25 |
| active | boolean | true = active codes only; false = inactive codes only |
| couponId | string | only codes that apply this coupon, from list_stripe_coupons |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"code": {
"type": "string",
"description": "customer-typable promotion code text to find"
},
"after": {
"type": "string",
"description": "pagination cursor: the last promotion code id from the previous page"
},
"limit": {
"type": "number",
"description": "rows to return, 1 to 100; default 25"
},
"active": {
"type": "boolean",
"description": "true = active codes only; false = inactive codes only"
},
"couponId": {
"type": "string",
"description": "only codes that apply this coupon, from list_stripe_coupons"
}
}
}Refunds on the brand's Stripe account, newest first: amount, status, reason and the charge refunded; filter by charge. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last refund id from the previous page |
| limit | number | rows to return, 1 to 100; default 25 |
| chargeId | string | only refunds of this charge, ch_... |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last refund id from the previous page"
},
"limit": {
"type": "number",
"description": "rows to return, 1 to 100; default 25"
},
"chargeId": {
"type": "string",
"description": "only refunds of this charge, ch_..."
}
}
}Subscriptions on the brand's OWN Stripe account with each item's price in MAJOR units (19.00 means $19.00), interval and quantity, and the MRR each contributes. With NO status it returns everything that counts toward MRR — active + trialing + past_due — and says so; pass status all, canceled, unpaid, incomplete, incomplete_expired or paused for the rest, or filter by customer or price. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor: the last subscription id from the previous page |
| limit | number | rows to return, 1 to 100; default 25 |
| status | string | filter by status (all, active, trialing, past_due, canceled and others); omit for MRR statuses |
| priceId | string | Stripe price id, price_...; only subscriptions on this price |
| customerId | string | Stripe customer id, cus_... |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor: the last subscription id from the previous page"
},
"limit": {
"type": "number",
"description": "rows to return, 1 to 100; default 25"
},
"status": {
"type": "string",
"description": "filter by status (all, active, trialing, past_due, canceled and others); omit for MRR statuses"
},
"priceId": {
"type": "string",
"description": "Stripe price id, price_...; only subscriptions on this price"
},
"customerId": {
"type": "string",
"description": "Stripe customer id, cus_..."
}
}
}List this workspace's SWIPEFILE — the saved-ad research board: every named collection and the ads/creatives kept in it (advertiser, headline, body copy, media URL, platform, when it was saved, and any taste tags). The SAME board the web app's Swipefile tab shows. Use it to answer "what have we saved?", to mine the user's own taste before planning an ad, or to find a reference to remix. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max ads to return (default 50, max 500) |
| collection | string | only list ads in this collection (by name or id) — omit for every collection |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max ads to return (default 50, max 500)"
},
"collection": {
"type": "string",
"description": "only list ads in this collection (by name or id) — omit for every collection"
}
}
}The Google Tag Manager containers SHARED WITH THIS PROFILE, with the exact containerPath every other Tag Manager tool needs. CALL THIS FIRST. A container is addressed by PATH, "accounts/<id>/containers/<id>", never by the GTM-XXXXXX public id a human reads off the snippet: no Tag Manager method accepts the public id, so passing one is refused. Each row carries the public id beside the path so you can match what the user says to what the API wants. THIS IS NOT EVERY CONTAINER THE GOOGLE ACCOUNT CAN SEE: one agency login routinely administers every client's container, and a container is not data about a client, it is control of what runs on their website, so the user ticks which containers belong to THIS profile and only those are reachable. An empty list means nothing is ticked yet: say so and point the user at Settings, Connectors, Google Tag Manager, Manage accounts (or call list_connector_accounts with provider google_tag_manager, then set_connector_accounts). Never name or guess a container. Read-only, free. Read-only, 0 credits. Needs Google Tag Manager connected.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}WHAT IS ACTUALLY FIRING ON THE SITE. Every tag in the container's workspace with its type, whether it is paused, which triggers fire it by name, and the one field that matters most: whether each tag is LIVE on the website or only STAGED in a draft. Also returns the container's triggers so you can reuse one instead of creating a duplicate. USE THIS TO ANSWER "is conversion tracking actually working": a Google Ads conversion action created by create_google_ads_conversion_action records nothing until a tag in this container fires it, and this is the only way to see whether that tag exists, whether it is paused, and whether it has been published. THREE VALUES FOR live, AND THEY ARE DIFFERENT ANSWERS: true means it is in the published container version and running, false means it is staged and NOT running yet, and null means the published version could not be read so you cannot tell. Never report null as false. If the response says the container has never been published, nothing in it is running at all. Read-only, free. 0 credits. Needs Google Tag Manager connected.
| Argument | Type | Description |
|---|---|---|
| workspace | string | older name for gtmWorkspace (MCP only) |
| gtmWorkspace | string | Tag Manager workspace id or name. Defaults to the Default Workspace, and the answer always states which one it read. |
| containerPath* | string | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers, never the GTM-XXXXXX public id, and it must be one SHARED with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"containerPath"
],
"properties": {
"workspace": {
"type": "string",
"description": "older name for gtmWorkspace (MCP only)"
},
"gtmWorkspace": {
"type": "string",
"description": "Tag Manager workspace id or name. Defaults to the Default Workspace, and the answer always states which one it read."
},
"containerPath": {
"type": "string",
"description": "the \"accounts/<id>/containers/<id>\" path from list_tag_manager_containers, never the GTM-XXXXXX public id, and it must be one SHARED with this profile"
}
}
}THE CONTAINER'S VERSION HISTORY, and the thing to read before proposing to publish or to roll one back. Every version with its id, its name, how many tags, triggers and variables it holds, whether it is archived, and which one is LIVE on the site right now. This is what makes a publish reversible: pass a row's path as publish_tag_manager_container's versionPath to put that version back on the site. The live flag is three-valued — true, false, or null when Google would not tell us which version is live, and null must never be read as false. Read-only. 0 credits. Needs Google Tag Manager connected.
| Argument | Type | Description |
|---|---|---|
| containerPath* | string | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers, never the GTM-XXXXXXX public id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"containerPath"
],
"properties": {
"containerPath": {
"type": "string",
"description": "the \"accounts/<id>/containers/<id>\" path from list_tag_manager_containers, never the GTM-XXXXXXX public id"
}
}
}List the members of the current profile — email, role (admin/member) and status. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}List the TikTok ADVERTISER accounts this profile can act on — id, name, currency, timezone and status. Every other TikTok Ads tool needs an advertiserId, and this is where it comes from: call this first and let the USER pick when there is more than one. Read-only, free, spends nothing. Needs TikTok ADS connected (Settings > Connectors > TikTok Ads). When nothing is shared and the user's TikTok login has no ad account at all, the reply says so: offer to create one with create_tiktok_ads_account.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}The conversion events an APP campaign may actually optimise toward on one app — the app twin of list_tiktok_ads_pixels, and the same distinction between "an event exists" and "an ad group can optimise for it". An event reporting INSUFFICIENT_POSTBACK is NOT broken and NOT misconfigured: TikTok needs more measured conversions before it will optimise toward it, and unlockThreshold says how many against conversions in the last 30 days. Only an event with usableForOptimization:true can be used today. A brand-new app with no measured conversions legitimately returns none. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| appId* | string | from list_tiktok_ads_apps |
| objective | string | default APP_INSTALL |
| advertiserId | string | from list_tiktok_ads_accounts |
| availableOnly | boolean | default true at TikTok — false also returns events that are not yet usable |
| placementType | string | default PLACEMENT_TYPE_AUTOMATIC |
| appPromotionType | string | APP_INSTALL or APP_RETARGETING; omit for TikTok's default |
| optimizationGoal | string |
The mobile apps registered on a TikTok ad account. An APP_INSTALL campaign cannot be built without one, and an APP-activity audience uses these ids as its event sources — so this is the missing lookup that made APP_INSTALL offerable and undeliverable in the same product, exactly as conversion campaigns were before pixels. Registering a NEW app is deliberately not offered here: it needs the store listing and the attribution provider, and it belongs in TikTok Ads Manager rather than being set on a user’s behalf. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}The custom audiences on a TikTok ad account — retargeting pools built from real behaviour (website pixel, app activity, ad engagement, organic posts, leads, shop activity) plus any lookalike grown from them. Until this existed, TikTok targeting through Hermoso was interests-and-geo only. THE FIELD THAT DECIDES EVERYTHING IS usableInAdGroups: TikTok takes up to 48 HOURS to process a new audience, so isValid:false immediately after creation is the normal processing window rather than a failure, and only a valid audience changes delivery. ownership tells you whether this advertiser owns the audience or another one shared it. detail:true with audienceIds reads /dmp/custom_audience/get/ instead — adding the rule, the auto-refresh setting and any error message TikTok recorded. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| detail | boolean | needs audienceIds — reads TikTok’s audience-detail endpoint instead, adding the rule and any error message it recorded |
| pageSize | number | up to 100 |
| audienceIds | array | up to 100 ids |
| advertiserId | string | from list_tiktok_ads_accounts — omit only when exactly one is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"detail": {
"type": "boolean",
"description": "needs audienceIds — reads TikTok’s audience-detail endpoint instead, adding the rule and any error message it recorded"
},
"pageSize": {
"type": "number",
"description": "up to 100"
},
"audienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 100 ids"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit only when exactly one is reachable"
}
}
}The ad account’s blocked-word list — a standing filter that hides ANY comment containing one of these words, automatically, across every ad on the account, before a human ever sees it. TikTok caps the list at 500 words per ad account and the reply says how many slots are left. Pass `check` with words to ask TikTok directly whether each one is already blocked — that answer is authoritative across the whole list rather than across the page you happened to fetch. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| check | array | ask whether these specific words are blocked — answered by TikTok, not by searching the page |
| pageSize | number | up to 500, which is the whole account limit — one page holds every word an account can have |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"check": {
"type": "array",
"items": {
"type": "string"
},
"description": "ask whether these specific words are blocked — answered by TikTok, not by searching the page"
},
"pageSize": {
"type": "number",
"description": "up to 500, which is the whole account limit — one page holds every word an account can have"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}The TikTok Business Centers reachable on this connection. THIS IS THE ONE PIECE OF STRUCTURE THAT MAKES THE CATALOG TOOLS DIFFERENT FROM EVERY OTHER TIKTOK TOOL: a product catalog belongs to a Business Center, not to an ad account, so the catalog tools take a bcId from here and an advertiserId will not work. With exactly one Business Center reachable the catalog tools resolve it themselves; with several they refuse and name them rather than guessing, because a catalog created under the wrong Business Center is invisible to the ad account that needed it and nothing in the reply would say so. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | filter to one |
| page | number | page number, from 1 |
| pageSize | number | up to 50 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bcId": {
"type": "string",
"description": "filter to one"
},
"page": {
"type": "number",
"description": "page number, from 1"
},
"pageSize": {
"type": "number",
"description": "up to 50"
}
}
}Read a TikTok advertiser account’s whole tree in one call — campaigns, ad groups and ads, each with the operation status that says whether it is enabled at all. Omit advertiserId when the brand reaches exactly one account; with several, the call names the choices rather than picking for you. THE THREE TIERS ARE READ SEPARATELY AND A TIER THAT FAILED IS REPORTED AS FAILED, never as empty — if the result carries a `partial` note, say which tier could not be read instead of telling the user they have no ads. Read-only, free. TikTok’s QPS is 1, so a big account reads back slowly: that is the throttle working, not a fault.
| Argument | Type | Description |
|---|---|---|
| advertiserId | string | from list_tiktok_ads_accounts — omit only when exactly one is reachable |
| includeReview | boolean | attach TikTok’s review verdict to every ad: approved / restricted / rejected, the reasons and TikTok’s suggested fix |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit only when exactly one is reachable"
},
"includeReview": {
"type": "boolean",
"description": "attach TikTok’s review verdict to every ad: approved / restricted / rejected, the reasons and TikTok’s suggested fix"
}
}
}The products inside a TikTok catalog, each with whether it can ACTUALLY be advertised and — when it cannot — TikTok’s own rejection reason together with its suggested fix and which placements and countries were affected. usableInAds is true only when all three of audit approved, active status ACTIVATED and availability IN_STOCK / AVAILABLE_FOR_ORDER / PREORDER hold, so a catalog full of "approved" products can still be entirely unadvertisable and the raw audit status alone would not say so. Narrow with productIds, skuIds or productSetIds; productIds and skuIds cannot be used together (TikTok’s own rule), and TikTok returns at most 10,000 rows in total so page x pageSize is capped rather than paged into a 400. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from list_tiktok_ads_business_centers |
| page | number | page number, from 1 |
| skuIds | array | up to 1,000 — not together with productIds |
| pageSize | number | up to 500 |
| catalogId* | string | from list_tiktok_ads_catalogs |
| productIds | array | up to 1,000 — not together with skuIds |
| productSetIds | array |
The product SETS in a TikTok catalog — the slices an ad group can target instead of the whole catalog — each with its filter conditions and how many products it holds. Product sets are built in TikTok Ads Manager rather than here, and this reads what exists. On a very large set pass productCount:false, because counting products can time the call out at TikTok’s end — that is TikTok’s own caveat, not a guess. A null productCount means it was not requested or not returned, not that the set is empty. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from list_tiktok_ads_business_centers |
| catalogId* | string | from list_tiktok_ads_catalogs |
| productCount | boolean | default true; pass false on a very large set to avoid a TikTok timeout |
| productSetId | string | filter to one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId"
],
"properties": {
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers"
},
"catalogId": {
"type": "string",
"description": "from list_tiktok_ads_catalogs"
},
"productCount": {
"type": "boolean",
"description": "default true; pass false on a very large set to avoid a TikTok timeout"
},
"productSetId": {
"type": "string",
"description": "filter to one"
}
}
}The product catalogs in a TikTok Business Center — the containers Dynamic Product Ads sell from — each with HOW MANY OF ITS PRODUCTS ARE APPROVED, REJECTED OR STILL PROCESSING, which is the difference between "the feed ran" and "the feed worked". A products:null on a row means those counts COULD NOT BE READ for that catalog, which is not the same as a catalog with no products. Pass regions:true to also get the regions a new catalog may target — worth doing BEFORE creating one, because a catalog’s region and currency are set once and can never be changed. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from list_tiktok_ads_business_centers — omit only when exactly one is reachable |
| page | number | page number, from 1 |
| regions | boolean | also return the regions a catalog in this Business Center may target |
| overview | boolean | set false to skip the per-catalog product counts (one extra call each) |
| pageSize | number | rows per page |
| catalogId | string | filter to one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers — omit only when exactly one is reachable"
},
"page": {
"type": "number",
"description": "page number, from 1"
},
"regions": {
"type": "boolean",
"description": "also return the regions a catalog in this Business Center may target"
},
"overview": {
"type": "boolean",
"description": "set false to skip the per-catalog product counts (one extra call each)"
},
"pageSize": {
"type": "number",
"description": "rows per page"
},
"catalogId": {
"type": "string",
"description": "filter to one"
}
}
}The public comments people have left on your TikTok ads — the read that finally makes TikTok comment moderation possible through Hermoso, where Meta and YouTube moderation already shipped. Covers both paid-impression ads and Spark Ads. TWO THINGS ARE NOT NEGOTIABLE AND BOTH COME FROM TIKTOK: comments are scoped to an AD GROUP and to nothing else (their search_field accepts the single value ADGROUP_ID, so there is no way to ask for one ad or a whole campaign — get ad group ids from list_tiktok_ads_campaigns), and startTime->endTime may span AT MOST 30 DAYS. That 30-day cap is not in TikTok’s parameter table; it was measured, and asking for more answers an opaque "The maximum allowed time span is 30 days" — so an empty result means "none in these 30 days", never "none ever", and covering a longer period means one call per slice. Every row carries what the other tools need: commentId, adId, tiktokItemId, identityId and identityType, plus hitBlockedWord (caught by your blocked-word list), isPinned, likes, replies, and canDelete — TikTok’s own per-comment answer to whether YOU may delete it, which is false for anything a member of the public wrote. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| endTime* | string | YYYY-MM-DD |
| pageSize | number | up to 100 |
| sortType | string | default DESC |
| adgroupId* | string |
The CRM Event sets on a TikTok ad account. A CRM Event Set is where LEAD-LIFECYCLE events go — a lead that became qualified, booked a demo, or closed — and sending those back with send_tiktok_ads_events (eventSource "crm", eventSourceId = the set’s id) is what lets TikTok optimise a LEAD_GENERATION campaign toward leads that actually convert rather than toward form fills. An advertiser is capped at fifty sets. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| name | string | filter by exact name |
| eventSetIds | array | filter to specific sets — TikTok accepts at most 50 ids |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "filter by exact name"
},
"eventSetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "filter to specific sets — TikTok accepts at most 50 ids"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}List the Custom Conversions defined on a TikTok EVENT SOURCE — narrower, rule-based conversions built on top of a pixel event (for example "Purchase, but only on /checkout/premium"). A Custom Conversion belongs to an event source, NOT to an advertiser, so TikTok requires eventSourceType and eventSourceId; with none given this resolves the single pixel on the advertiser when there is exactly one and otherwise asks which. Pass a Custom Conversion to create_tiktok_ads_ad_group as customConversionId to optimise toward the narrow rule instead of the broad standard event. TikTok accepts it ONLY alongside a pixel and only when optimizationGoal is CONVERT or IN_APP_EVENT, and only when its own optimizationEvent matches the one on the ad group; usableForAds reports whether its activity status allows it. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}List the IDENTITIES on a TikTok advertiser account — the TikTok accounts an ad is allowed to appear as. THIS IS A HARD PREREQUISITE, not a convenience: a TikTok ad carries no identity of its own and there is NO DEFAULT, so create_tiktok_ads_ad refuses without an id from here. Call it, show the user the list, and let them CHOOSE — an ad runs publicly under whichever account is named, so picking one for them is a public mistake on somebody else’s profile. If it comes back empty, no TikTok account has been authorised on this advertiser yet and nothing can be advertised from it; say that rather than guessing an id. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| advertiserId | string | from list_tiktok_ads_accounts — omit to use the connected default |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit to use the connected default"
}
}
}List the real ORGANIC TikTok posts published by one identity — the account itself, not the ad library. THIS IS WHERE A SPARK AD STARTS: pass an itemId from here to create_tiktok_ads_ad as tiktokItemId and the ad IS that post, with its own comments, likes and shares, running under the account that made it. Use this for an account the brand controls or has been granted (identityType TT_USER or BC_AUTH_TT); for a CREATOR’s post they authorised with a code, use list_tiktok_ads_spark_posts instead. identityId and identityType both come from list_tiktok_ads_identities, and a BC_AUTH_TT identity ALSO needs the identityAuthorizedBcId reported next to it — TikTok refuses the call without it. TikTok defaults to VIDEO posts only, so pass itemType:"CAROUSEL" to see photo posts; an account that posts only photos otherwise looks empty. Paginated by CURSOR, not page: pass back the cursor while hasMore is true. posterUrl and previewUrl EXPIRE AFTER AN HOUR — show them, never store them. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| count | number | 1-20; TikTok ignores anything larger and returns 20 |
| cursor | string | from the previous call, while hasMore is true |
| itemType | string | TikTok defaults to VIDEO only — pass CAROUSEL for photo posts |
| identityId* | string | REQUIRED — from list_tiktok_ads_identities |
| advertiserId | string | from list_tiktok_ads_accounts |
The questions a TikTok Instant Form asks — which is exactly the set of columns download_tiktok_ads_leads will return. It is NOT a fixed schema: it is whatever that form was built to ask, so read it before promising a user which fields their leads carry. leadSource DIRECT_MESSAGE reads the fields collected in the associated Business Account’s direct messages instead, and takes no pageId (TikTok says it is "not supported" there). Read-only, free.
| Argument | Type | Description |
|---|---|---|
| pageId | string | the Instant Form, from list_tiktok_ads_lead_forms. Required for INSTANT_FORM. |
| leadSource | string | INSTANT_FORM (default) or DIRECT_MESSAGE |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"pageId": {
"type": "string",
"description": "the Instant Form, from list_tiktok_ads_lead_forms. Required for INSTANT_FORM."
},
"leadSource": {
"type": "string",
"description": "INSTANT_FORM (default) or DIRECT_MESSAGE"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}The Instant Forms (lead forms) on a TikTok ad account — this is where the pageId every other lead tool needs comes from. A form whose status is EDITED is a DRAFT and has never been able to collect anything, which is a different thing from "a form with no leads". THERE IS NO API THAT CREATES AN INSTANT FORM: they are built in TikTok Ads Manager, so an advertiser with none needs one made there (or a LEAD_GENERATION campaign using promotionTargetType EXTERNAL_WEBSITE, which collects on your own site and has no form here). Read-only, free.
| Argument | Type | Description |
|---|---|---|
| title | string | exact-match name filter |
| status | string | PUBLISHED or EDITED (draft) |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"title": {
"type": "string",
"description": "exact-match name filter"
},
"status": {
"type": "string",
"description": "PUBLISHED or EDITED (draft)"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| scene | string | CAROUSEL_ADS when searching; CREATIVE_ASSET for the creative-tools library |
| keyword | string | search text for the music library |
| musicIds | array | music ids to look up |
| pageSize | number | rows to fetch from TikTok; the reply shows the first 30 |
| imageUrls | array | 2-35 URLs of images already uploaded to the ad account |
| searchType | string | SEARCH_BY_KEYWORD, SEARCH_BY_RECOMMEND, SEARCH_BY_LIKED, SEARCH_BY_HISTORY, SEARCH_BY_MUSIC_ID or SEARCH_BY_SOURCE |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
The Offline Event sets on a TikTok ad account — the containers real-world conversions are reported into: an in-store purchase, a phone booking, a signed contract, a call-centre sale. An id here is what send_tiktok_ads_offline_events reports into, and what send_tiktok_ads_events wants as eventSourceId when eventSource is "offline". The reply also counts how many are AUTO-TRACKING, which matters because an auto-tracking set is attached to every campaign created afterwards and TikTok caps an advertiser at ten of them. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| name | string | filter by exact name |
| eventSetIds | array | filter to specific sets |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "filter by exact name"
},
"eventSetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "filter to specific sets"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Which ad accounts draw money from one TikTok payment portfolio, and which users are allowed to spend from it. Two separate TikTok calls answering two separate questions, so one failing does not empty the other; a half that could not be read says so rather than reporting an empty list. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| show | string | default both |
| pageSize | number | 1-50 |
| paymentPortfolioId* | string | from list_tiktok_ads_payment_portfolios |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"paymentPortfolioId"
],
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"show": {
"enum": [
"advertisers",
"users",
"both"
],
"type": "string",
"description": "default both"
},
"pageSize": {
"type": "number",
"description": "1-50"
},
"paymentPortfolioId": {
"type": "string",
"description": "from list_tiktok_ads_payment_portfolios"
}
}
}The payment portfolios under a TikTok Business Center; how a customer’s ad accounts are actually funded, whether the money is pooled across them (an Advanced portfolio) or held per account (a Standard one), how many accounts draw on each, and what credit line it carries. WORTH READING WHEN CAMPAIGNS STOP DELIVERING, because that is very often a funding answer rather than an ads answer, and nothing else in Hermoso can see it. By TikTok’s own default this lists every portfolio belonging to the same CLIENT as the Business Center, including ones not linked to it; pass bcRelated:true to narrow it. READ-ONLY BY DESIGN: Hermoso never creates a portfolio, moves an ad account onto one, or allocates a credit line, because each of those decides where a customer’s money sits. Free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from list_tiktok_ads_business_centers |
| page | number | page number, from 1 |
| type | string | SHARED is Advanced (pooled funds), NON_SHARED is Standard (per-account) |
| pageSize | number | 1-50 |
| bcRelated | boolean | true narrows to portfolios actually linked to this Business Center |
| paymentPortfolioIds | array | filter, max 10 |
List the conversion pixels on a TikTok ad account, each with the EVENTS it can optimise toward — which is the difference between "a pixel exists" and "this campaign can actually optimise". Call it before building any conversion campaign (WEB_CONVERSIONS, CONVERSIONS, PRODUCT_SALES, LEAD_GENERATION): those need optimizationGoal CONVERT, which TikTok refuses without a pixel. Only an event reported under optimizationEvent can be used — TikTok returns none for PAGE_VIEW because it "cannot be used for optimization". An EMPTY event list is not proof a pixel is idle: TikTok refreshes pixel event data every 2-4 hours, so a pixel that started firing recently still reports []. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| name | string | fuzzy name filter |
| pixelId | string | filter to one pixel |
| advertiserId | string | from list_tiktok_ads_accounts — omit only when exactly one is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "fuzzy name filter"
},
"pixelId": {
"type": "string",
"description": "filter to one pixel"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit only when exactly one is reachable"
}
}
}List the playables in a TikTok advertiser's Creative Library: id, name, playable_url, orientation, and TikTok's check / review status (VALIDATE_SUCCESS can be saved; only AUDIT_SUCCESS delivers). Free.
| Argument | Type | Description |
|---|---|---|
| name | string | filter by playable name |
| page | number | page number, from 1 |
| limit | number | 1-100, default 20 |
| status | string | VALIDATING, VALIDATE_SUCCESS, VALIDATE_FAIL, AUDIT_SUCCESS or AUDIT_FAIL; omit for all |
| stored | string | default SAVED (in the library) |
| playableId | string | one playable id to read |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "filter by playable name"
},
"page": {
"type": "number",
"description": "page number, from 1"
},
"limit": {
"type": "number",
"description": "1-100, default 20"
},
"status": {
"enum": [
"VALIDATING",
"VALIDATE_SUCCESS",
"VALIDATE_FAIL",
"AUDIT_SUCCESS",
"AUDIT_FAIL"
],
"type": "string",
"description": "VALIDATING, VALIDATE_SUCCESS, VALIDATE_FAIL, AUDIT_SUCCESS or AUDIT_FAIL; omit for all"
},
"stored": {
"enum": [
"SAVED",
"UPLOADED",
"DELETED"
],
"type": "string",
"description": "default SAVED (in the library)"
},
"playableId": {
"type": "string",
"description": "one playable id to read"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}THE AUTOMATED RULES RUNNING ON A TIKTOK AD ACCOUNT, and — the part that matters — which of them can move money. Every row carries canSpend, the specific actions that put it there (armingActions), whether its reach is UNBOUNDED (a rule targeting "all active campaigns" re-resolves that set at every run, so it covers campaigns created after it was written), and a scopeToken. That token is what set_tiktok_ads_rule_status wants echoed back before it will arm a spending rule, and it is computed from the rule AS TIKTOK STORES IT — so it also detects a rule that has been edited in TikTok Ads Manager since it was read. The reply leads with the rules that are armed AND able to spend, because "what is running on my account that I am not watching" is the actual question. Pass ruleIds for specific rules, or filter by status / dimension / name. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| tzone | string | timezone string for the rule's time windows; optional |
| status | string | ON, OFF or DELETED; omit for all |
| ruleIds | array | specific rules — omit to list them all |
| pageSize | number | rows per page |
| ruleInfo | array | rule ids or rule names to filter by |
List the TikTok posts a post OWNER has authorised this ad account to promote — the creator-collaboration half of Spark Ads. These are posts from accounts the brand does NOT control: the owner generates an authorization code in the TikTok app and authorize_tiktok_ads_spark_post applies it, after which the post appears here with the itemId create_tiktok_ads_ad needs as tiktokItemId. AN EMPTY LIST IS A NORMAL STARTING STATE, not a failure — it means nobody has authorised a post yet, and the fix is to ask a creator for a code, not to retry. Each row carries the authorization WINDOW (authStartTime / authEndTime): a Spark Ad cannot outlive its authorization, so check the end time before building a campaign around one. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1; see totalPage in the reply |
| keyword | string | free text (fuzzy, ≤500 chars) or an exact TikTok post id (≥19 digits) |
| pageSize | number | 1-50, default 20 |
| itemTypes | array | defaults to BOTH here (unlike list_tiktok_ads_identity_posts, which defaults to VIDEO alone) |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1; see totalPage in the reply"
},
"keyword": {
"type": "string",
"description": "free text (fuzzy, ≤500 chars) or an exact TikTok post id (≥19 digits)"
},
"pageSize": {
"type": "number",
"description": "1-50, default 20"
},
"itemTypes": {
"type": "array",
"items": {
"enum": [
"VIDEO",
"CAROUSEL"
],
"type": "string"
},
"description": "defaults to BOTH here (unlike list_tiktok_ads_identity_posts, which defaults to VIDEO alone)"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}The products inside a TikTok Shop, with titles, images, price ranges, currency and historical sales. Pass adCreationEligible to also learn which of them may ACTUALLY be advertised; CUSTOM_SHOP_ADS for Shopping Ads, GMV_MAX for a Product GMV Max campaign; and note that WITHOUT it TikTok does not report eligibility at all, so an absent flag means unknown rather than yes. That filter additionally requires an advertiserId, because eligibility is a fact about a product AND an ad account rather than about a product alone. This call is keyed on a BUSINESS CENTER, which list_tiktok_ads_stores reports on each store row. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from the store row, or from list_tiktok_ads_business_centers |
| page | number | page number, from 1 |
| storeId* | string | from list_tiktok_ads_stores |
| pageSize | number | 1-100, default 10 |
| sortType | string | ASC or DESC |
| sortField | string | min_price or historical_sales |
| productName | string | filter by product title |
The TikTok Shops granted to a TikTok ad account; what Shopping Ads and GMV Max campaigns sell from. Each row also names the BUSINESS CENTER that can reach the shop, and that is what list_tiktok_ads_store_products needs, because the product endpoint is keyed on a Business Center rather than on this ad account. An empty result is a real answer rather than an error: a shop has to be created in TikTok Shop Seller Center and granted to the ad account, and this reads that grant rather than creating one. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| storeId | string | filter to one |
| storeType | string | TIKTOK_SHOP; the only store type |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"storeId": {
"type": "string",
"description": "filter to one"
},
"storeType": {
"enum": [
"TIKTOK_SHOP"
],
"type": "string",
"description": "TIKTOK_SHOP; the only store type"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Which identity or business documents TikTok accepts for verification in a given country; the list differs everywhere, so there is no useful default and the country is required. Returns each document’s name and the code submit_tiktok_ads_verification needs. business_type BUSINESS asks about company paperwork (an EIN letter, a certificate of incorporation); INDIVIDUAL asks about personal identification (passport, driver’s licence, residence card). Read-only, free.
| Argument | Type | Description |
|---|---|---|
| regionIsoCode* | string | the ISO country of the ACCOUNT being verified, e.g. US: tiktok_ads_verification_status reports it |
| verificationType | string | default BUSINESS |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"regionIsoCode"
],
"properties": {
"regionIsoCode": {
"type": "string",
"description": "the ISO country of the ACCOUNT being verified, e.g. US: tiktok_ads_verification_status reports it"
},
"verificationType": {
"enum": [
"BUSINESS",
"INDIVIDUAL"
],
"type": "string",
"description": "default BUSINESS"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| labelType | string | default SEARCH |
| ttoAccountId | string | from list_tiktok_tto_accounts: omit only when exactly one is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"labelType": {
"enum": [
"SEARCH",
"RANKING"
],
"type": "string",
"description": "default SEARCH"
},
"ttoAccountId": {
"type": "string",
"description": "from list_tiktok_tto_accounts: omit only when exactly one is reachable"
}
}
}Read the Smart+ campaigns on a TikTok advertiser. TikTok’s reads LAG its writes, so a campaign created seconds ago can be missing here and still exist — the reply says so rather than reporting an empty account.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1; the reply says when there are more |
| limit | number | page size, 1-1000, default 20 |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1; the reply says when there are more"
},
"limit": {
"type": "number",
"description": "page size, 1-1000, default 20"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}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 arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| pageSize | number | rows per page |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
| brandProfileIds | array | filter, max 20 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"pageSize": {
"type": "number",
"description": "rows per page"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
},
"brandProfileIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "filter, max 20"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| pageSize | number | 1-5 |
| campaignIds | array | max 5 |
| campaignType | string | TikTok defaults to CAMPAIGN: ask for BRAND_LINK to see brand-level ones |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"pageSize": {
"type": "number",
"description": "1-5"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "max 5"
},
"campaignType": {
"enum": [
"CAMPAIGN",
"BRAND_LINK"
],
"type": "string",
"description": "TikTok defaults to CAMPAIGN: ask for BRAND_LINK to see brand-level ones"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| handles | array | max 50 |
| pageSize | number | 1-50, default 10 |
| campaignIds | array | max 50 |
| campaignType | string | omit to see every campaign |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"handles": {
"type": "array",
"items": {
"type": "string"
},
"description": "max 50"
},
"pageSize": {
"type": "number",
"description": "1-50, default 10"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "max 50"
},
"campaignType": {
"enum": [
"CAMPAIGN",
"BRAND_LINK"
],
"type": "string",
"description": "omit to see every campaign"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
}
}
}Deployments on the brand's Vercel account, newest first: URL, state (READY, BUILDING, QUEUED, ERROR, CANCELED...), target (production or preview), when it was created and became ready, the commit message and Vercel's inspector link. Filter by projectId, target or state, or pass deploymentId (an id or a deployment URL) to read one, e.g. to see whether a page you just deployed is READY. Read-only, free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-50, default 20 |
| state | string | filter to deployments in this state |
| until | string | the nextCursor a previous page returned — the next (older) page |
| target | string | filter by environment: production, preview or staging |
| projectId | string | project id or name |
| deploymentId | string | read ONE deployment by id or URL |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1-50, default 20"
},
"state": {
"enum": [
"BLOCKED",
"BUILDING",
"CANCELED",
"DELETED",
"ERROR",
"INITIALIZING",
"QUEUED",
"READY"
],
"type": "string",
"description": "filter to deployments in this state"
},
"until": {
"type": "string",
"description": "the nextCursor a previous page returned — the next (older) page"
},
"target": {
"enum": [
"production",
"preview",
"staging"
],
"type": "string",
"description": "filter by environment: production, preview or staging"
},
"projectId": {
"type": "string",
"description": "project id or name"
},
"deploymentId": {
"type": "string",
"description": "read ONE deployment by id or URL"
}
}
}The domains on the brand's Vercel account (verified or not, when they expire), or with projectId the domains assigned to one project (and any redirect or git branch on them). Read-only, free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-50, default 20 |
| projectId | string | project id or name, for that project's domains |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1-50, default 20"
},
"projectId": {
"type": "string",
"description": "project id or name, for that project's domains"
}
}
}The brand's Vercel projects (its websites): name, id, framework, production URL and state, the latest deployment (URL, state, target, when), the linked git repo, and whether Web Analytics and Speed Insights are on. search filters by project name. Read-only, free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-50, default 20 |
| cursor | string | nextCursor from a previous call |
| search | string | part of a project name |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1-50, default 20"
},
"cursor": {
"type": "string",
"description": "nextCursor from a previous call"
},
"search": {
"type": "string",
"description": "part of a project name"
}
}
}List the X (Twitter) ad accounts this profile can act on, with the PERMISSION LEVEL held on each so you can tell an admin grant from a read-only one before attempting a write. X grants API access PER AD ACCOUNT, not per app: the customer adds Hermoso’s X user to their ad account at business.x.com -> Account access, and it appears here. Read-only, free.
No arguments.
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {}
}List the custom audiences (customer lists) on an X ad account — size, whether each can be targeted yet and why not. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max audiences to return |
| query | string | name prefix |
| shared | boolean | lists shared with this account instead of owned |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| audienceIds | array | return only these audience ids |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max audiences to return"
},
"query": {
"type": "string",
"description": "name prefix"
},
"shared": {
"type": "boolean",
"description": "lists shared with this account instead of owned"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"audienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "return only these audience ids"
}
}
}List campaigns on an X ad account — status, budgets, and whether X considers each servable. Omit adAccountId when only one account is reachable and it resolves itself. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of campaigns to return |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of campaigns to return"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}List the cards on an X ad account with their card_uri and destination. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max cards to return, 1-200; default 50 |
| query | string | name prefix |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max cards to return, 1-200; default 50"
},
"query": {
"type": "string",
"description": "name prefix"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}List the funding instruments (payment methods) on an X ad account — type, currency, credit limit, credit remaining, and whether each can currently fund a campaign. THIS IS THE ANSWER TO “why is my X campaign not delivering?” whenever the cause is a cancelled card or an exhausted credit line, which is invisible from the campaign itself, and it shows what a campaign will spend against BEFORE anyone activates it. Hermoso cannot add a payment method — that is done at ads.x.com. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}List the lead forms on an X ad account (or one, with leadFormId) — fields, privacy policy, and the customer-list audience X builds from each form's leads. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of lead forms to return |
| accountId | string | older name for adAccountId (MCP only) |
| leadFormId | string | one lead form id; omit for all forms |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| withDeleted | boolean | include archived forms |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of lead forms to return"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"leadFormId": {
"type": "string",
"description": "one lead form id; omit for all forms"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"withDeleted": {
"type": "boolean",
"description": "include archived forms"
}
}
}List the line items on an X ad account — X's name for an ad group, and the level that carries the objective, the placements, the bid, the targeting and the creatives. Pass campaignId to scope it to one campaign. `servable` is X's own verdict on whether the line item could run. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of line items to return |
| accountId | string | older name for adAccountId (MCP only) |
| campaignId | string | scope to one campaign's line items; omit for all |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of line items to return"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "scope to one campaign's line items; omit for all"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}List the X ad account's Media Library — media keys usable in cards and promoted-only posts. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max items to return, 1-50; default 20 |
| accountId | string | older name for adAccountId (MCP only) |
| mediaType | string | IMAGE, VIDEO or GIF; omit for all types |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max items to return, 1-50; default 20"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"mediaType": {
"enum": [
"IMAGE",
"VIDEO",
"GIF"
],
"type": "string",
"description": "IMAGE, VIDEO or GIF; omit for all types"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}List the posts an X ad account can promote — promoted-only ones by default (timeline NULLCAST), or ORGANIC / ALL. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | max posts to return |
| asUserId | string | promotable user id whose posts to list; omit for the ad account's own user |
| timeline | string | NULLCAST = promoted-only posts (default); ORGANIC = organic posts; ALL = both |
| tweetIds | array | return only these post ids |
| accountId | string | older name for adAccountId (MCP only) |
| tweetType | string | post state filter: DRAFT, PUBLISHED or SCHEDULED; omit for all |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max posts to return"
},
"asUserId": {
"type": "string",
"description": "promotable user id whose posts to list; omit for the ad account's own user"
},
"timeline": {
"enum": [
"ALL",
"NULLCAST",
"ORGANIC"
],
"type": "string",
"description": "NULLCAST = promoted-only posts (default); ORGANIC = organic posts; ALL = both"
},
"tweetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "return only these post ids"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"tweetType": {
"enum": [
"DRAFT",
"PUBLISHED",
"SCHEDULED"
],
"type": "string",
"description": "post state filter: DRAFT, PUBLISHED or SCHEDULED; omit for all"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}The X accounts this ad account may promote — FULL (can be promoted for followers with create_x_ads_promoted_account and have its posts promoted) or RETWEETS_ONLY (posts only). Read-only, free.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}List the promoted posts (the CREATIVES) attached to an X line item, or across the whole ad account. Two things only this can tell you: each post's APPROVAL STATUS, so an ad X rejected — which can never serve however the statuses are set — is visible rather than mysterious; and the promoted-tweet ID, which is the only way to remove one. NOTE a promoted post cannot be PAUSED on X (its PUT accepts only an approval appeal), so the reversible way to stop it is to pause its LINE ITEM. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of promoted posts to return |
| accountId | string | older name for adAccountId (MCP only) |
| lineItemId | string | scope to one line item |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of promoted posts to return"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"lineItemId": {
"type": "string",
"description": "scope to one line item"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}List every targeting criterion an X line item carries, WITH THE ID of each — the id needed to remove one with delete_x_ads_object. READ THE EMPTY CASE CORRECTLY: no targeting criteria on X means the line item is UNRESTRICTED and will reach the broadest possible audience once ACTIVE — it does NOT mean it cannot serve. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| lineItemId* | string | X line item id from list_x_ads_line_items |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"lineItemId"
],
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"lineItemId": {
"type": "string",
"description": "X line item id from list_x_ads_line_items"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}MARK WHAT YOU DID ON THE CUSTOMER'S OWN GA4 CHARTS — "ad launched", "post went out", "price change" — so the traffic it moved is visible where they already look. action:'list' reads them (Google's own system annotations included, which cannot be edited or deleted), 'create' adds one on a single date or a startDate–endDate range, 'update' changes one, 'delete' removes one (confirm-gated). color: PURPLE BROWN BLUE GREEN RED CYAN ORANGE (default BLUE). Every write is read back. Free.
| Argument | Type | Description |
|---|---|---|
| date | string | YYYY-MM-DD — one day |
| color | string | chart colour; default BLUE |
| title | string | the label on the chart. Required to create |
| action | string | defaults to 'list' |
| confirm | boolean | must be true to actually delete |
| endDate | string | last day of a range, YYYY-MM-DD; use with startDate instead of date |
| property* | string | NUMERIC GA4 property id, from list_analytics_properties |
| startDate | string |
THE DERIVED NUMBER A MARKETER ACTUALLY REPORTS — cost per purchase, revenue per session, a margin — built from metrics GA4 already collects and then available to analytics_report like any other metric. action:'list' reads them, 'create' adds one, 'update' changes the formula, name, unit or description, 'delete' removes one (confirm-gated). THE ID IS CHOSEN ONCE AND IS PERMANENT: calculatedMetricId becomes the API name reports ask for (calcMetric:your_id), Google marks it output-only afterwards, and renaming is refused by name rather than silently ignored — so pick it deliberately. The formula takes + - * / and parentheses over metric names and plain numbers, referencing at most 5 unique custom metrics; Google answers a syntax error with the exact position. WATCH invalidMetricReference ON THE WAY BACK: a formula naming a metric this property does not collect is created happily and flagged invalid, and any report using it may fail or return unexpected numbers — report that rather than a clean success. Free.
| Argument | Type | Description |
|---|---|---|
| action | string | defaults to 'list' |
| confirm | boolean | must be true to actually delete |
| formula | string | e.g. "advertiserAdCost / conversions". Required to create |
| property* | string | NUMERIC GA4 property id, from list_analytics_properties |
| metricUnit | string | STANDARD | CURRENCY | FEET | MILES | METERS | KILOMETERS | MILLISECONDS | SECONDS | MINUTES | HOURS. Required to create |
HOW GA4 DECIDES WHICH BUCKET A CLICK LANDS IN, which is the number an ad studio is judged on and the answer to "why is my campaign showing as Unassigned". action:'list' reads every group with its channels in the order GA4 tests them, including Google's own Default channel group — read that first, because a campaign landing in the wrong channel is a rule that does not match rather than missing data. 'create' authors your own group, 'update' changes one, 'delete' removes one (confirm-gated). AUTHORING: `channels` is an ordered list, each with a name and its conditions, and GA4 STOPS AT THE FIRST RULE THAT MATCHES — so a broad rule above a narrow one silently swallows it. A condition is {fieldName, matchType, value} or {fieldName, values:[…]} for a list, plus not:true to invert; an entry that is itself an ARRAY of conditions is OR'ed, and separate entries are AND'ed. THE FIELD NAMES ARE NOT THE REPORT DIMENSIONS: use eachScopeSource, eachScopeMedium, eachScopeCampaignName, eachScopeCampaignId, eachScopeSourcePlatform or eachScopeDefaultChannelGroup — sessionSource, medium and the rest are real GA4 dimensions that Google REFUSES here, and its refusal does not say what to use instead. `primary:true` makes the group the one every report is bucketed by AND unsets whichever group was primary before. Google's own Default channel group cannot be edited and is refused by name. An update REPLACES the whole rule set. Free.
| Argument | Type | Description |
|---|---|---|
| action | string | defaults to 'list' |
| confirm | boolean | must be true to actually delete |
| primary | boolean | make this the group reports are bucketed by. It unsets the previous primary group, changing what every report in the property shows |
THE SERVER-SIDE EVENT CHANNEL FOR GA4, the Google twin of the conversions APIs Hermoso already ships for Reddit, Snapchat and OpenAI Ads. A Measurement Protocol secret is the api_secret half of https://www.google-analytics.com/mp/collect; the other half is the stream's measurementId for a web stream or its firebaseAppId for an app stream, and this tool reports whichever applies. action:"list" shows every secret with its value, action:"create" mints one (displayName is required, and it is the ONLY label a secret ever gets because the values are opaque), action:"delete" revokes one and NEEDS confirm:true. TELL THE USER THIS BEFORE THEY DELETE: there is no rotation anywhere in the API, because the value is output-only and patch reaches only the display name. A delete takes effect at once, and anything still sending with the old value stops being recorded SILENTLY, since the Measurement Protocol answers a bad api_secret with a 2xx and drops the hit. Rotating safely means create the new one, move every sender across, then delete the old one. Free.
| Argument | Type | Description |
|---|---|---|
| action | string | defaults to 'list' |
| confirm | boolean | must be true to actually delete |
| property* | string | NUMERIC GA4 property id |
| secretId | string | for action:'delete', from action:'list' — preferred over displayName because it is unambiguous |
| dataStream | string |
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.
| Argument | Type | Description |
|---|---|---|
| asset | object | update — the new content |
| level | string | campaign, adGroup or customer; follows the id you pass |
| action | string | default list |
| dryRun | boolean | true = validate with Google and change nothing |
| assetId | string | asset id, from the list action |
| confirm | boolean | true only after the user agreed to change a LIVE campaign's assets |
| adGroupId | string | ad group id, when level is adGroup |
| assetType | string |
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.
| Argument | Type | Description |
|---|---|---|
| id | string | remove |
| ages | array | age ranges to include: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown |
| apps | array | app identifiers for a custom segment (create_custom_segment) |
| kind | string | remove |
| name | string | audience or list name, unique on the account (create actions) |
| type | string | custom segment type: AUTO (Google decides, default) or SEARCH (people who searched the keywords) |
| urls | array |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | strategy name, for create or update |
| action | string | default list |
| dryRun | boolean | validate with Google, change nothing |
| confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
| strategy | string | create |
| maxCpcUsd | number | max CPC ceiling (required for TARGET_IMPRESSION_SHARE) |
| minCpcUsd | number | min CPC floor |
| customerId |
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.
| Argument | Type | Description |
|---|---|---|
| action | string | default 'read' |
| dryRun | boolean | validate with Google, write nothing |
| confirm | boolean | REQUIRED true for set and clear, after the user approved |
| budgetId | string | numeric campaign budget id; omit for the account default |
| settings | array | for set: one entry per campaign type |
| 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 |
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.
| Argument | Type | Description |
|---|---|---|
| ads | array | ads to label, as a list of {adGroupId, adId} objects |
| name | string | label name, for create or update |
| color | string | #RRGGBB |
| action | string | default list |
| dryRun | boolean | validate with Google, change nothing |
| confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
| labelId | string | the label (from action list) |
| adGroupIds | array | ad group ids to apply or remove the label on |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | create |
| action | string | default list |
| dryRun | boolean | validate with Google, change nothing |
| confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
| keywords | array | keywords to block, as text strings or {text, matchType BROAD, PHRASE or EXACT} objects |
| customerId | string | omit to use the selected default account |
| campaignIds | array |
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.
| Argument | Type | Description |
|---|---|---|
| action* | string | apply (live now; spend types need confirm), dismiss (remove from list), or auto_apply (set auto-apply per type) |
| dryRun | boolean | validate and change nothing |
| confirm | boolean | the user saw the list and said yes |
| autoApply | object | auto_apply: {RECOMMENDATION_TYPE: true|false} |
| customerId | string | 10-digit account id, omit for the brand’s default account |
| confirmCount | number | apply: echo the count the unconfirmed call reported |
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.
| Argument | Type | Description |
|---|---|---|
| kind | string | for action:'create' — which kind to make. Passing propertyId implies the Google Analytics one |
| action | string | defaults to 'list' |
| confirm | boolean | must be true to actually archive |
| propertyId | string | NUMERIC GA4 property id for a Google Analytics link (from list_analytics_properties), not a G- measurement id |
| displayName |
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.
| Argument | Type | Description |
|---|---|---|
| action | string | defaults to 'list' |
| callBackUri | string | HTTPS endpoint Google will POST to (create). Not a Hermoso URL and not localhost |
| targetAccount | string | accounts/{id} to receive notifications for; defaults to this account |
| subscriptionId | string | for action:'delete', from action:'list' |
| registeredEvent | string | defaults to PRODUCT_STATUS_CHANGE, the one that fires on a disapproval |
| merchantCenterId* | string | from list_merchant_accounts |
| allManagedAccounts |
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.
| Argument | Type | Description |
|---|---|---|
| uri | string | homepage set_uri |
| action | string | defaults to the section's read (get/list) |
| policy | object | returns create |
| address | object | business_info: regionCode, street or addressLines, locality, administrativeArea, postalCode |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | create: ≤255 characters |
| action* | string | create, add, remove, attach, detach or delete a shared negative keyword list |
| listId | string | every action but create |
| confirm | boolean | true to delete the list, or to remove, detach or delete on a LIVE campaign |
| keywords | array | keywords for create or add: [{text, matchType}] |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
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.
| Argument | Type | Description |
|---|---|---|
| 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 |
| confirm | boolean | REQUIRED true to delete |
| deleteIt | boolean | true to delete the catalog named by catalogId |
| accountId | string | older name for adAccountId (MCP only) |
| catalogId | string | set to UPDATE or DELETE an existing catalog; omit to create a new one |
| merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
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.
| Argument | Type | Description |
|---|---|---|
| action | string | default add |
| keywords | array | add: the keywords |
| parentId | string | add: the ad group, or a campaign for negative keywords |
| keywordIds | array | archive: ids from list_pinterest_ads_keywords |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"action": {
"enum": [
"add",
"archive"
],
"type": "string",
"description": "default add"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"type": "string",
"description": "the search words, up to 120 characters"
},
"matchType": {
"enum": [
"BROAD",
"PHRASE",
"EXACT",
"EXACT_NEGATIVE",
"PHRASE_NEGATIVE"
],
"type": "string",
"description": "default BROAD; a campaign takes only the _NEGATIVE types"
}
}
},
"description": "add: the keywords"
},
"parentId": {
"type": "string",
"description": "add: the ad group, or a campaign for negative keywords"
},
"keywordIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "archive: ids from list_pinterest_ads_keywords"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| code | string | the code a buyer redeems |
| type | string | create: the kind of offer |
| title | string | create: the internal name |
| action | string | default create |
| fields | object | any other promotion field, by Pinterest’s own name |
| confirm | boolean | delete: required true |
| endTime | number | Unix seconds |
| customId | string | your own id for it |
| startTime | number |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | create: the template name |
| action | string | default create |
| keywords | array | keywords to hold |
| templateId | string | remove: from pinterest_ads_account_data what:targeting_templates |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| targetingSpec | object | Pinterest targeting object, e.g. {"GEO":["US"],"AGE_BUCKET":["25-34"]} |
| placementGroup | string | create: default ALL |
| autoTargetingEnabled | boolean | create: default true at Pinterest |
Add, rename or delete a tab in a Google Spreadsheet. action:"add" + title · action:"rename" + tab + newTitle · action:"delete" + tab. Name the tab by its TITLE or its numeric sheetId (list_sheet_tabs gives both); an unknown tab is refused with the real list rather than a Google error nobody can map back. DELETING a tab destroys everything on it: call it without confirm first to get the filled-cell count, then confirm:true + confirmCells. Google does not allow removing the LAST remaining tab in a file, and that is refused by name with the way out (clear it, or delete the whole file with delete_drive_file). Every action is read back from the spreadsheet before it is reported as done.
| Argument | Type | Description |
|---|---|---|
| tab | string | which tab — its title or numeric sheetId (rename / delete) |
| title | string | the name for the new tab (action:"add") |
| action* | string | add, rename or delete a tab |
| confirm | boolean | true deletes a tab that holds data; needs confirmCells |
| newTitle | string | what to rename the tab to (action:"rename") |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| confirmCells | number |
Have Snapchat PUSH every new lead from a lead form to Hermoso the moment it is submitted — the ONLY way Snapchat hands leads to an API (it has no endpoint that lists leads), so leads submitted before this was on are never delivered. action 'subscribe' (default) turns it on, read back from Snapchat; forwardTo (public https) also relays each lead to a CRM, Zapier or Make. 'list' shows the form's webhooks; 'test' makes Snapchat send its sample lead and waits for it to ARRIVE at Hermoso, which proves delivery end to end; 'delete' removes a webhook (confirm:true; one Hermoso did not make may feed the advertiser's own CRM, and the refusal says so). Snapchat allows ONE webhook per form and requires Organization Admin on the Snapchat organization. Every delivery is verified against Snapchat's HMAC signature before it is stored. Read leads with list_snapchat_ads_leads. Free.
| Argument | Type | Description |
|---|---|---|
| action | string | default subscribe |
| formId | string | the lead form (list_snapchat_ads_lead_forms) |
| confirm | boolean | REQUIRED true for delete |
| forwardTo | string | optional public https URL each lead is also forwarded to |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| integrationId | string |
Change the ad account’s blocked-word list — the standing filter that auto-hides any comment containing one of these words across every ad on the account. operation "add" (up to TikTok’s 500-word account limit, refused here for free if the list would overflow rather than failing at TikTok), "remove", or "replace" one word with another. Warning: REMOVING A WORD REPUBLISHES HISTORY: TikTok’s own words are that comments already hidden by it "will become public instead of being visible only to the commentor" — a bulk unhide across every ad on the account, for which TikTok reports no count. So a remove without confirm changes nothing and tells you which of the words are actually on the list first. EVERY WRITE HERE IS READ BACK: TikTok answers all three operations with an empty response and its delete explicitly does not error on a word that was never on the list, so reporting off the response would report a no-op as a change — the reply is the diff of the list before and after.
| Argument | Type | Description |
|---|---|---|
| words | array | for add and remove |
| confirm | boolean | REQUIRED true for remove — without it nothing is removed and you get the list of which words are actually blocked plus what unhiding them will do |
| newWord | string | for replace |
| oldWord | string | for replace |
| operation* | string | add (append words), remove (needs confirm to unhide), or replace (oldWord swapped for newWord) |
The scheduled import that fills a TikTok catalog from the product feed a store already publishes — Shopify, WooCommerce and BigCommerce all expose one, so this is usually the whole job of getting a merchant’s products into TikTok. THE ONE THING TO GET RIGHT IS updateMode, and it has NO DEFAULT ON PURPOSE: INCREMENTAL creates new products and updates existing ones, while OVERWRITE IS A FULL REPLACEMENT THAT REMOVES EVERY PRODUCT THE FEED OMITS — pointing a partial feed at a populated catalog in OVERWRITE mode deletes the rest of it. uri, intervalType and timezone are ALL-OR-NOTHING; TikTok requires each of the three whenever either of the others is given, so a URL with no schedule is a rejected call rather than a one-off fetch. If any additional-image URL inside the feed contains a comma it must be URL-encoded as %2C, or TikTok splits it into two URLs (Shopify-style transform parameters are the common case). Products do not appear instantly: the feed runs on its schedule and then TikTok reviews the products, so check list_tiktok_ads_catalog_products and tiktok_ads_catalog_diagnostics rather than expecting them immediately. action:"delete" is confirm-gated and stops FUTURE imports — the products already imported stay in the catalog.
| Argument | Type | Description |
|---|---|---|
| uri | string | http/https/ftp/sftp URL of the feed file — CSV, TSV or XML (RSS/ATOM), up to 8GB |
| bcId | string | from list_tiktok_ads_business_centers |
| hour | number | 0-23 |
| action | string | default list |
| feedId |
Create, rename or delete an Offline Event set — the container in-store and other real-world conversions are reported into. AUTOTRACKING IS A STANDING SETTING RATHER THAN A PROPERTY OF THIS SET: with it on, EVERY campaign created under this advertiser afterwards attributes to it automatically, including campaigns nobody has thought of yet. TikTok caps an advertiser at ten auto-tracking sets, and once that is reached a non-auto-tracking set cannot be switched over until one is deleted — so leave it off unless the user means it. DELETING is confirm-gated, and not because of the container: every conversion already reported into it stops being available to reporting and optimisation, campaigns tracking it lose their attribution, and TikTok publishes no undelete. A create is READ BACK from TikTok, because the create response is only an id and never the stored row. Free.
| Argument | Type | Description |
|---|---|---|
| name | string | required for create; max 40 characters, and TikTok refuses a name already used on the account |
| action* | string | create, update or delete (delete needs confirm) |
| confirm | boolean | REQUIRED true to delete |
| eventSetId | string | required for update and delete — from list_tiktok_ads_offline_event_sets |
| description | string | free-text description of the event set; optional |
TikTok’s own test-lead mechanism — the way to prove a lead pipeline works end to end WITHOUT waiting for a real person to fill in a form, and the exact twin of testEventCode on send_tiktok_ads_events. action "create" makes one, "get" reads the existing one, "delete" removes it. A test lead flows through the SAME download path as a real one (so download_tiktok_ads_leads is provably wired), carries obviously-dummy campaign and ad-group names so it can never be mistaken for a customer, spends nothing and reaches nobody. TikTok allows EXACTLY ONE test lead per Instant Form, so delete the existing one before creating another. Free.
| Argument | Type | Description |
|---|---|---|
| action* | string | create | get | delete |
| leadId | string | required for delete — get it from action:"get" |
| pageId | string | the Instant Form, from list_tiktok_ads_lead_forms — required for create/get on an INSTANT_FORM |
| leadSource | string | INSTANT_FORM (default) or DIRECT_MESSAGE |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"action"
],
"properties": {
"action": {
"enum": [
"create",
"get",
"delete"
],
"type": "string",
"description": "create | get | delete"
},
"leadId": {
"type": "string",
"description": "required for delete — get it from action:\"get\""
},
"pageId": {
"type": "string",
"description": "the Instant Form, from list_tiktok_ads_lead_forms — required for create/get on an INSTANT_FORM"
},
"leadSource": {
"type": "string",
"description": "INSTANT_FORM (default) or DIRECT_MESSAGE"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| action | string | default list |
| anchorId | string | required for delete |
| pageSize | number | 1-50 |
| anchorIds | array | filters a list, max 100 |
| anchorName | string | required for create: an internal label TikTok never shows viewers, ≤32 characters |
WHY THE MERCHANT CENTER ACCOUNT IS OR IS NOT SERVING, in one read. Do not trust the program state on its own: an account can report Shopping ads and free listings as ENABLED and serve in ZERO countries, because Google counts a region as active only where every requirement for that program is met. This reports the active regions, the unmet requirements Google names with its own help link for each, and then the eight account settings that explain them — whether the homepage is CLAIMED (unclaimed stops the whole account serving), whether business info has an address, a verified phone and a customer service contact, whether any active shipping service covers the countries you sell to, the return policies, whether the Merchant Center terms have been accepted, plus autofeed and automatic improvements. An account with no shipping settings at all is reported as not configured, which is an account state and not a failed read. Anything that genuinely could not be read comes back null with a warning, which means unknown rather than missing. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}GOOGLE'S OWN FIX STEPS, not just the name of the problem. list_merchant_issues stops at the symptom; this returns the remediation content Google writes for each issue, plus the ACTIONS available on it — request a review, claim the website, edit a named attribute — with the exact opaque ids trigger_merchant_issue_action needs. Pass an offerId (with feedLabel, and contentLanguage if it is not 'en') for one PRODUCT's issues, or leave it off for the ACCOUNT's. The fix text is Google's and is relayed verbatim: RELAY IT, do not paraphrase a remediation step into something that sounds simpler. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| offerId | string | for one PRODUCT's issues; omit for the account's issues |
| timeZone | string | IANA zone for times inside the content, e.g. 'America/Los_Angeles' |
| feedLabel | string | REQUIRED alongside offerId — from list_merchant_products. A product is addressed by contentLanguage + feedLabel + offerId |
| languageCode | string | BCP-47 language for the fix text, e.g. 'en-US' |
| contentLanguage | string | with offerId; defaults to 'en' |
| merchantCenterId* | string | from list_merchant_accounts |
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.
| Argument | Type | Description |
|---|---|---|
| merchantCenterId* | string | from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}COMPETITOR INTELLIGENCE GOOGLE COMPUTES FOR FREE, for any retail brand with a Merchant Center. Ten report views, read with a SQL-like MCQL query. The three worth reaching for first: `competitive_visibility_competitor_view` (WHICH other domains appear beside this merchant, their rank, page-overlap and higher-position rates — i.e. who is actually beating them), `best_sellers_product_cluster_view` (what is SELLING on Google in a category right now, with an inventory_status saying whether this merchant even stocks it) and `price_insights_product_view` (Google's own suggested price plus the predicted click and conversion change). Also: competitive_visibility_benchmark_view, competitive_visibility_top_merchant_view, best_sellers_brand_view, price_competitiveness_product_view, product_view, product_performance_view, non_product_performance_view. MCQL IS NOT SQL: no OR, no subqueries, no GROUP BY, no aggregates, no JOIN, and ORDER BY may only name fields already in SELECT. Date filters use `WHERE date BETWEEN '2026-01-01' AND '2026-01-31'` or `WHERE date DURING LAST_30_DAYS`. Several views REQUIRE specific fields in SELECT and in WHERE — competitive visibility needs report_category_id + report_country_code + traffic_source, and top_merchant uniquely REQUIRES a date condition while FORBIDDING date in SELECT. An unknown view is refused by name with the list. NOT AVAILABLE ON MULTI-CLIENT (MCA) ACCOUNTS — pass a subaccount id. An empty result is often a normal state (price insights are only produced where Google predicts a substantial gain), and the note says which kind of empty it is. Read-only, free, no new permission.
| Argument | Type | Description |
|---|---|---|
| query* | string | the MCQL query, e.g. "SELECT id, title, price, suggested_price, effectiveness FROM price_insights_product_view" |
| pageSize | number | rows per response, 1–100000 (default 1000). 100,000 is GOOGLE's ceiling, not ours. |
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.
| Argument | Type | Description |
|---|---|---|
| name* | string | name for the new merged audience, at least 3 characters |
| audienceIds* | array | 2 to 64 DISTINCT audience ids from list_openai_ads_audiences |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"audienceIds"
],
"properties": {
"name": {
"type": "string",
"description": "name for the new merged audience, at least 3 characters"
},
"audienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "2 to 64 DISTINCT audience ids from list_openai_ads_audiences"
}
}
}Use this when the user asks how their Facebook or Instagram ads performed. Pull performance INSIGHTS (spend, impressions, reach, clicks, CTR, CPC, CPM, conversions) for a connected ad account, or a specific campaign / ad set / ad. Pass adAccountId (for auth); optionally objectId to scope to one object and level to break the numbers down. BREAKDOWNS are what make the numbers actionable — a flat total says an ad cost $X, never WHO it worked on: pass breakdowns:"age,gender", "publisher_platform,platform_position" (which placement), "country" / "region" / "dma" (where), "impression_device" / "device_platform" (what they held). Comma-separated; "placement", "device" and "geo" are accepted as aliases; an unknown value is REJECTED, never silently ignored. THREE breakdowns need an ad-account OPT-IN from 2026-08-06 — impression_device, hourly_stats_aggregated_by_audience_time_zone and frequency_value: Meta returns NO ROWS (not an error) for an account that has not opted in, so they are always ATTEMPTED, and if nothing comes back the report is re-run WITHOUT them and `droppedBreakdowns` + a note name the missing dimension and say an account admin can enable it in Ads Manager. A dropped dimension is ABSENT, never zero — never present the remaining total as if it were still split by it. Date window: datePreset OR since+until (YYYY-MM-DD). datePreset is Meta's OWN enum — today, yesterday, last_3d, last_7d, last_14d, last_28d, last_30d, last_90d, this_week_mon_today, this_week_sun_today, last_week_mon_sun, last_week_sun_sat, this_month, last_month, this_quarter, last_quarter, this_year, last_year, maximum, data_maximum. THERE IS NO "lifetime": Meta disabled it in Graph API v10.0 and replaced it with "maximum" (the last 37 months); anything unrecognised is refused by name here rather than 400ing at Meta. Read-only.
| Argument | Type | Description |
|---|---|---|
| level | string | break the numbers down by this level |
| since | string | start date YYYY-MM-DD (use with until). Meta keeps 37 months: an older start is moved to the oldest day Meta has, and the note says so |
Who ELSE is competing for the same Microsoft Advertising auctions — the other advertisers’ domains with their impression share, overlap rate, position-above rate, top-of-page rate and outranking share. microsoft_ads_report tells you what the account did; this tells you who it did it against, which is the input to a bid or a creative decision rather than a report. Scope it with entityType Account (default, the whole account) / Campaign / AdGroup / Keyword plus entityIds (max 200), and optionally segment by up to three of Day, DayOfWeek, Device. SAY THIS WHEN REPORTING: every figure is a share of the auctions YOUR ads entered, never a measure of a competitor’s whole account or budget — a competitor missing from the list did not necessarily not bid. An empty result means there was no delivery to compare in that window, which is MISSING data; never report it as "nobody is bidding against you". Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| since | string | YYYY-MM-DD, default 30 days back |
| until | string | YYYY-MM-DD, default today |
| segments | array | at most three — Microsoft’s own limit |
| accountId | string | older name for adAccountId (MCP only) |
| entityIds | array | REQUIRED for Campaign / AdGroup / Keyword, max 200. Microsoft only defaults to the whole account for entityType Account |
| entityType |
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.
| Argument | Type | Description |
|---|---|---|
| types | array | narrow to some of ResponsiveSearchAdsOpportunity, MultiMediaAdsOpportunity, RemoveConflictingNegativeKeywordOpportunity, FixConversionGoalSettingsOpportunity, CreateConversionGoalOpportunity, all five by default |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"types": {
"type": "array",
"items": {
"type": "string"
},
"description": "narrow to some of ResponsiveSearchAdsOpportunity, MultiMediaAdsOpportunity, RemoveConflictingNegativeKeywordOpportunity, FixConversionGoalSettingsOpportunity, CreateConversionGoalOpportunity, all five by default"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| campaignId | string | omit for the whole account |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "omit for the whole account"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}Export a Microsoft Advertising account as a BULK FILE — one spreadsheet holding whatever slice of the account you ask for. This is the ONLY way to read most of a Microsoft account: roughly 185 of Microsoft’s record types are reachable here and nowhere else in Hermoso — sitelink / callout / structured-snippet / image / logo / price / promotion ad extensions, labels, shared negative keyword lists, bid strategies, audiences and their associations, experiments, seasonality adjustments, conversion goals, asset groups and listing groups, feeds. Pass entities[] (default Campaigns, AdGroups, Ads, Keywords), campaignIds[] to narrow it, or since for a DELTA of what changed (Microsoft refuses anything older than 30 days). The reply carries the WHOLE FILE as a string in `file` — edit that and pass it straight to microsoft_ads_bulk_upload, which is the round trip this exists for. A bulk export is a SNAPSHOT of what Microsoft held when it ran, never a live view, so do not quote a status from it as current after a change. Microsoft builds it asynchronously: a large account can come back pending:true with a downloadRequestId — call again with that id rather than resubmitting, which would export the whole account a second time. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| limit | number | how many rows to return inline, default 200 (the full file is still in `file`) |
| since | string | ISO date — export only what CHANGED since then. Microsoft refuses a delta older than 30 days, and refuses one at all alongside QualityScoreData / BidSuggestionsData |
| waitMs | number | how long to wait for Microsoft to build the file before returning pending, max 120000 |
| entities |
Apply a Microsoft Advertising BULK FILE — edit hundreds of campaigns, ad groups, ads, keywords, extensions or labels in one request. Pass `file` as the CSV/TSV string; microsoft_ads_bulk_download returns exactly that shape in its `file` field. THE SPEND GATE, AND IT IS UNLIKE EVERY OTHER TOOL HERE. A bulk file carries a Status column, so one upload can turn campaigns ON — and confirm:true ALONE IS NOT ENOUGH for a file: it proves you meant to upload something, not that you know what THIS file does, and it reads identically for a file that changes two bids and one that activates fifty campaigns. So: call it ONCE with no confirmation. Nothing is uploaded, and you are told exactly how many objects the file would ACTIVATE (they can then spend real money) and DELETE (which Microsoft never undoes — a deleted object is never returned by any read again), each named with its row number, type, id and name. SHOW THAT LIST TO THE USER, get an explicit yes, then call again with confirm:true, confirmActivations:<n> and confirmDeletions:<n>. If the activations are incidental — a file you round-tripped out of a download and only meant to edit bids in — pass pauseInstead:true instead and those rows are written as Paused, so everything else lands with nothing armed; it is refused when the file also deletes, because pausing is a safe substitute for activating and there is none for deleting. Partial success is normal on this API: rows Microsoft rejects come back listed individually and the correct ones ARE applied, so never describe a CompletedWithErrors upload as a failure. 0 credits.
| Argument | Type | Description |
|---|---|---|
| file* | string | the bulk file itself, CSV or TSV, whose first column header must be "Type" |
| dryRun | boolean | report what the file would do and upload nothing at all |
| waitMs |
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.
| Argument | Type | Description |
|---|---|---|
| adIds | array | the ads to explain |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId* | string | the ad group the ads or keywords sit in |
| keywordIds | array | or the keywords to explain (not both) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId"
],
"properties": {
"adIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the ads to explain"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "the ad group the ads or keywords sit in"
},
"keywordIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "or the keywords to explain (not both)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}Resolve country / region / city names to the Microsoft Advertising location ids that create_microsoft_ads_campaign needs. Read-only, free, 0 credits. Use it when a location ask is ambiguous ("Springfield") — this returns EVERY candidate with its id so the USER can pick, and you never guess between two places. Accepts names, ISO country codes ("CA"), or numeric location ids. Pass `query` as ONE ask (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.
| Argument | Type | Description |
|---|---|---|
| query* | any | 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. |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"query": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "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."
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| url | string | a landing page for Microsoft to mine ideas from |
| network | string | search network the volume is measured on: Microsoft-owned, partner-syndicated or both |
| keywords | array | seed terms to expand from |
| language | string | default English |
| accountId | string | older name for adAccountId (MCP only) |
| attributes | array | which idea attributes to return — omit for all |
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.
| Argument | Type | Description |
|---|---|---|
| limit | number | Microsoft’s MaxCount, omit for everything |
| types | array | 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 |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| includeDismissed | boolean |
Performance for a Microsoft Advertising account — impressions, clicks, CTR, average CPC, spend, conversions. `reportType` picks WHICH report, and that is the whole Microsoft reporting surface, not just campaigns: AdGroupPerformance, AdPerformance, KeywordPerformance, SearchQueryPerformance (the actual search terms people typed), GeographicPerformance, UserLocationPerformance, AgeGenderAudience and ProfessionalDemographicsAudience (LinkedIn-sourced job function and industry, inside Bing), ConversionPerformance, DestinationUrlPerformance, ShareOfVoice, AssetPerformance, ProductDimensionPerformance, SearchCampaignChangeHistory ("what changed on Tuesday") and ~30 more — an unknown name is refused WITH the full list rather than forwarded. `aggregation` controls the row grain (Summary / Daily / Hourly / Weekly / Monthly / Yearly / HourOfDay / DayOfWeek). Two reports keep far less history than the usual 36 months — AssetPerformance 30 days, ShareOfVoice 6 — and the reply says so, because an empty short-retention report is a retention limit, not an absence of delivery. Window via timePeriod (Today | Yesterday | LastSevenDays | Last14Days | Last30Days | ThisWeek | LastWeek | LastFourWeeks | ThisMonth | LastMonth | LastThreeMonths | LastSixMonths | ThisYear | LastYear | ThisWeekStartingMonday | LastWeekStartingMonday | LastFourWeeksStartingMonday) or since+until (YYYY-MM-DD) — default Last30Days. An unrecognised timePeriod is REFUSED, never silently swapped for another window. Microsoft generates reports ASYNCHRONOUSLY: this can return pending:true with a reportRequestId, and you must call again rather than reporting any numbers. A report that succeeds with ZERO rows genuinely means there was no delivery in that window — say exactly that; never present zeros as measured performance. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| since | string | YYYY-MM-DD custom range start (with until) |
| until | string |
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.
| Argument | Type | Description |
|---|---|---|
| maxCpc* | number | REQUIRED — the max CPC bid to estimate at, in the account currency |
| network | string | search network to estimate for; same values as microsoft_ads_keyword_ideas network |
| keywords* | array | the keywords to estimate |
| language | string | language to estimate for, e.g. English |
| accountId | string | older name for adAccountId (MCP only) |
| matchType | string | default Exact |
| adAccountId | string |
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.
| Argument | Type | Description |
|---|---|---|
| on | string | a property expression to break the funnel down by |
| unit | string | the bucket the results are grouped into |
| limit | number | top N property values; Mixpanel defaults to 255, max 10000, and it does nothing unless "on" is set |
| where | string | a segmentation expression to filter by |
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.
| Argument | Type | Description |
|---|---|---|
| bookmarkId* | string | the saved report's bookmark id, from its URL in Mixpanel |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"bookmarkId"
],
"properties": {
"bookmarkId": {
"type": "string",
"description": "the saved report's bookmark id, from its URL in Mixpanel"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| page | number | pages start at ZERO; any page above 0 REQUIRES sessionId |
| where | string | a Mixpanel segmentation expression over profile properties, e.g. properties["$city"] == "London" |
| cohortId | string | a numeric cohort id from list_mixpanel_cohorts |
| sessionId | string | from the previous page — required for page > 0 |
| distinctId | string | one person |
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.
| Argument | Type | Description |
|---|---|---|
| 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 |
| unit | string | the interval unit (default day) |
| event | string | the returning event; omitted means any event |
| limit | number | top N segmentation values; does nothing unless "on" is set |
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.
| Argument | Type | Description |
|---|---|---|
| on | string | a segmentation expression to break down by, e.g. properties["$browser"] |
| type | string | count type: general (event count, default), unique (distinct users) or average |
| unit | string | NOTE: Mixpanel offers no "week" on segmentation, unlike retention |
| event* | string | a SINGLE event name — not an array |
| limit | number | top N property values; Mixpanel defaults to 60, max 10000, and it does nothing unless "on" is set |
| where | string | a segmentation expression to filter by |
Hiding takes a comment out of public view — everyone stops seeing it except the person who wrote it, who is never told — and it works on ANYONE’s comment, which is what distinguishes it from delete_tiktok_ads_comment (that one only ever removes your own reply). IT IS FULLY REVERSIBLE: the same tool with operation PUBLIC puts a comment back, which is why it needs no confirmation. Takes a list, so a whole batch of spam goes in one call. TikTok answers this endpoint with no data at all, so the reply reports what was ACCEPTED and tells you to re-run list_tiktok_ads_comments to see commentStatus change — it is not a read-back and does not pretend to be. To hide a whole class of comment automatically and permanently, add the word to the account’s blocked-word list instead (manage_tiktok_ads_blocked_words).
| Argument | Type | Description |
|---|---|---|
| operation* | string | HIDDEN takes them out of public view; PUBLIC puts them back |
| commentIds* | array | from list_tiktok_ads_comments |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"commentIds",
"operation"
],
"properties": {
"operation": {
"enum": [
"HIDDEN",
"PUBLIC"
],
"type": "string",
"description": "HIDDEN takes them out of public view; PUBLIC puts them back"
},
"commentIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "from list_tiktok_ads_comments"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}action list (default): every sharing link and person with access to a OneDrive file or folder: link URL, view or edit, scope, expiry, password, whether it is inherited from a parent folder. action remove: revoke one (permissionId from the list); anyone relying on it loses access at once and the link cannot be restored, so WITHOUT confirm:true nothing is removed and you get what would go. An inherited permission is removed on the parent folder, and the owner's own access cannot be removed. Removal is verified by reading the list back. 0 credits. Needs OneDrive connected.
| Argument | Type | Description |
|---|---|---|
| action | string | list (default) or remove |
| fileId* | string | OneDrive file or folder id |
| confirm | boolean | required true for remove |
| permissionId | string | for remove |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"action": {
"enum": [
"list",
"remove"
],
"type": "string",
"description": "list (default) or remove"
},
"fileId": {
"type": "string",
"description": "OneDrive file or folder id"
},
"confirm": {
"type": "boolean",
"description": "required true for remove"
},
"permissionId": {
"type": "string",
"description": "for remove"
}
}
}Create or update up to 1,000 ChatGPT Ads campaigns, ad groups and ads in ONE asynchronous job. It builds CHAT-CARD campaigns only: its campaign.create has no product-feed mode, and OpenAI’s spreadsheet bulk creation of product-feed campaigns and ad groups with auto-generated ad templates (announced 2026-09-30) is an Ads Manager feature with no API. Build a product-feed campaign with create_openai_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.
| Argument | Type | Description |
|---|---|---|
| operations |
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.
| Argument | Type | Description |
|---|---|---|
| after | string | the last operation_id from the previous page — only available once complete is true |
| jobId* | string | the id openai_ads_bulk returned |
| limit | number | 1–100 results per page, default 100 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"jobId"
],
"properties": {
"after": {
"type": "string",
"description": "the last operation_id from the previous page — only available once complete is true"
},
"jobId": {
"type": "string",
"description": "the id openai_ads_bulk returned"
},
"limit": {
"type": "number",
"description": "1–100 results per page, default 100"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id to report on; sets the level to ad |
Look up ChatGPT Ads location ids by name — countries, regions and DMAs — so a campaign can be geo-targeted. GEO AND CUSTOM AUDIENCES ARE THE ONLY LIST-BASED TARGETING THIS PLATFORM HAS: there are no interests, no lookalikes, no age or gender. Everything else is semantic, through an ad group’s context hints. Custom audiences are targeted with customAudienceIds / excludedCustomAudienceIds (see list_openai_ads_audiences). Pass the returned ids as locationIds when creating or updating a campaign; a campaign with no location targeting runs everywhere available. OpenAI opens new markets without notice (Indonesia, Malaysia, the Philippines, Singapore, Thailand, Vietnam and Taiwan went live 2026-09-30), so search a market rather than assuming from memory that ChatGPT Ads does not serve it. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| limit | number | maximum number of locations to return |
| query* | string | a place name, e.g. "Toronto" or "United Kingdom" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "number",
"description": "maximum number of locations to return"
},
"query": {
"type": "string",
"description": "a place name, e.g. \"Toronto\" or \"United Kingdom\""
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adIds | array | return only these ad ids |
| after | string | pagination cursor for the next page |
| limit | number | property/time-bucket groups per page, up to 2000 |
| since* | string | YYYY-MM-DD, account timezone |
| until* | string | YYYY-MM-DD, account timezone, no later than today |
| filters |
Performance for ChatGPT Ads — impressions, clicks, spend, CTR, CPC, CPM, and CONVERSIONS with CPA, post-click conversion rate and attributed order sales / ROAS, in the ad account currency. OpenAI returns conversions only with granularity none or daily and with no segment or a country/device segment (never platform or product), and CPA, conversion rate and sales only with no segment at all; the report adds every column OpenAI allows for the shape you asked and its note names any it left out, so a missing column is their limit, not a zero. Since OpenAI’s 2026-09-30 change these conversions count click-through PLUS view-through goal conversions within the reporting windows (default 30-day click, 1-day view, dated by the ad interaction); CPA uses them and post-click conversion rate uses click-through only. clickWindowDays (7, 14, 30), viewWindowDays (0 = clicks only, or 1) and attributionTimeBasis choose those windows for every conversion column and never change the campaign; use the same ones as Ads Manager when comparing, and the reply’s `attribution` names the windows used. The scope follows the id you pass: none = the whole ad account, or campaignId / adGroupId / adId. PRODUCT-FEED CAMPAIGNS serving in the multi-product CAROUSEL unit also report per-card numbers: ask for them in `fields` — carousel_product_card_impressions, carousel_product_card_clicks, product_impressions, product_clicks, product_spend, product_ctr, product_cpc, product_cpm plus product_title / product_price / product_feed_id and the other product_* fields (complete from 2026-08-20 on a rolling 30-day basis); they come back under each row’s `fields`. A card impression counts when a product card becomes viewable and is NOT a billable impression, so never add it to spend math. granularity is hourly, daily, monthly or none (default daily); the default window is the last 30 days; segment by country or device for a breakdown, and level rolls the rows up by campaign / ad group / ad. A report with NO rows genuinely means there was NO delivery in that window — say exactly that; never present zeros as measured performance. attributedEvents:true adds every event OpenAI attributed beyond the campaign goal (purchases on a sign-up campaign) for the same scope and days; never add those counts to conversions. Read-only and free, so run it FIRST after connecting: it proves the key works with zero spend risk.
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.
| Argument | Type | Description |
|---|---|---|
| date | string | metrics_ready_state: YYYY-MM-DD |
| what* | string | which data |
| bookmark | string | the next page, from the last reply |
| lookback | string | conversion_eqs: default 14d |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| searchQuery | string | targeting_templates: by name |
| includeSizing | boolean | targeting_templates: add the audience size |
| sourcePlatform | string | conversion_eqs: e.g. WEB |
| ingestionSource |
The DEEP Pinterest ad report — Pinterest’s ASYNCHRONOUS lane, which reaches 914 DAYS back (2.5 years) where pinterest_ads_report stops at 90, and carries roughly three times the metric columns (conversion, ROAS and cross-device families the quick report does not have). Use it for anything older than three months, and for revenue questions. Levels: ADVERTISER / CAMPAIGN / AD_GROUP / PIN_PROMOTION / KEYWORD / PRODUCT_GROUP / PRODUCT_ITEM plus their *_TARGETING twins. Pinterest generates it asynchronously, so this may come back pending:true with a token — CALL AGAIN WITH THAT TOKEN to pick it up, and never re-submit without it (a second submit generates a second report). Pinterest’s own windows are enforced here with the reason rather than as an opaque 400: 914 days back over at most 186 days; at HOUR granularity 8 days back over 3; at a PRODUCT_ITEM level 92 back over 31. A finished report link is valid five minutes and the report one hour, so an EXPIRED status means run it again, not that anything failed. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| adIds | array | limit the report to these ad ids |
| level | string | ADVERTISER | CAMPAIGN | AD_GROUP | PIN_PROMOTION | KEYWORD | PRODUCT_GROUP | PRODUCT_ITEM (+ _TARGETING variants) — default CAMPAIGN. An unknown level is refused with the list. |
| since | string | YYYY-MM-DD (default 30 days ago) |
| token | string | RESUME a pending report — pass the token back instead of re-submitting |
| until | string |
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.
| Argument | Type | Description |
|---|---|---|
| keywords | array | keywords to size |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| creativeTypes | array | e.g. REGULAR, VIDEO, SHOPPING |
| targetingSpec | object | Pinterest targeting object, the same one an ad group takes |
| placementGroup | string | default ALL |
| productGroupIds | array | product groups to size |
| autoTargetingEnabled | boolean | default true at Pinterest |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"type": "string",
"description": "the search words, up to 120 characters"
},
"matchType": {
"enum": [
"BROAD",
"PHRASE",
"EXACT",
"EXACT_NEGATIVE",
"PHRASE_NEGATIVE"
],
"type": "string",
"description": "default BROAD; a campaign takes only the _NEGATIVE types"
}
}
},
"description": "keywords to size"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"creativeTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "e.g. REGULAR, VIDEO, SHOPPING"
},
"targetingSpec": {
"type": "object",
"description": "Pinterest targeting object, the same one an ad group takes",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"placementGroup": {
"enum": [
"ALL",
"SEARCH",
"BROWSE",
"OTHER"
],
"type": "string",
"description": "default ALL"
},
"productGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "product groups to size"
},
"autoTargetingEnabled": {
"type": "boolean",
"description": "default true at Pinterest"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| action | string | default download |
| 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 |
| entityIds | array | download: only these ids |
| requestId | string | status: the id download or upsert answered |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
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.
| Argument | Type | Description |
|---|---|---|
| mode | string | related (default), suggested (searches that begin with the term) or metrics (search volume) |
| limit | number | suggested: 1–10 |
| terms* | array | keywords or seed terms; suggested mode uses the first one only |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| countryCode | string | metrics: two-letter country, default US |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"terms"
],
"properties": {
"mode": {
"enum": [
"related",
"suggested",
"metrics"
],
"type": "string",
"description": "related (default), suggested (searches that begin with the term) or metrics (search volume)"
},
"limit": {
"type": "number",
"description": "suggested: 1–10"
},
"terms": {
"type": "array",
"items": {
"type": "string"
},
"description": "keywords or seed terms; suggested mode uses the first one only"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"countryCode": {
"type": "string",
"description": "metrics: two-letter country, default US"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adIds | array | REQUIRED for ad level — Pinterest refuses /ads/analytics without it, unless you pass pinIds AND campaignIds instead |
| level | string | which level to report at — omit and it is inferred from the ids you pass (none -> the whole account) |
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.
| Argument | Type | Description |
|---|---|---|
| refresh | boolean | default true (recompute if stale); false = return the cached result even if stale |
| insightId | string | omit to list the saved insights and their ids |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"refresh": {
"type": "boolean",
"description": "default true (recompute if stale); false = return the cached result even if stale"
},
"insightId": {
"type": "string",
"description": "omit to list the saved insights and their ids"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| string | exact email match | |
| limit | number | 1–100, default 20 |
| search | string | free-text search across person properties |
| distinctId | string | PostHog distinct id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"email": {
"type": "string",
"description": "exact email match"
},
"limit": {
"type": "number",
"description": "1–100, default 20"
},
"search": {
"type": "string",
"description": "free-text search across person properties"
},
"distinctId": {
"type": "string",
"description": "PostHog distinct id"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | row cap, 1–1000 (default 1000). A LIMIT already in the query is honoured and clamped to the same ceiling |
| query* | string | a HogQL (ClickHouse-flavoured SQL) query — one bounded question, never a page of an export |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "number",
"description": "row cap, 1–1000 (default 1000). A LIMIT already in the query is honoured and clamped to the same ceiling"
},
"query": {
"type": "string",
"description": "a HogQL (ClickHouse-flavoured SQL) query — one bounded question, never a page of an export"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-500, default 100 |
| since | string | ISO timestamp |
| until | string | ISO timestamp |
| offset | number | skip this many responses; use the nextOffset from the previous reply |
| scoreGte | number | needs questionId; e.g. 9 for NPS promoters |
| scoreLte | number | needs questionId; e.g. 6 for NPS detractors |
| surveyId* | string |
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.
| Argument | Type | Description |
|---|---|---|
| adId | string | a YouTube video ad id |
| adGroupId | string | its ad group id |
| customerId | string | 10-digit account id, omit for the brand’s default account |
| assetGroupIds | array | Performance Max asset group ids |
| loginCustomerId | string | manager id if operating through an MCC |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "a YouTube video ad id"
},
"adGroupId": {
"type": "string",
"description": "its ad group id"
},
"customerId": {
"type": "string",
"description": "10-digit account id, omit for the brand’s default account"
},
"assetGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Performance Max asset group ids"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}See a LinkedIn ad exactly as LinkedIn renders it: desktop and mobile feed preview links straight from LinkedIn (they expire after ~3 hours — ask again for fresh ones), plus the ad’s REVIEW status and, if LinkedIn rejected it, its rejection reasons and serving holds. Works for single-image, carousel, video, job and event ads; LinkedIn’s Ad Preview API renders none for text-only or document ads, and the reply says so. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| creativeId* | string | ad creative id, from list_linkedin_ads_campaigns or create_linkedin_ads_creative |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"creativeId"
],
"properties": {
"creativeId": {
"type": "string",
"description": "ad creative id, from list_linkedin_ads_campaigns or create_linkedin_ads_creative"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
}
}
}Render a REAL preview of a Meta ad, per placement — Meta returns a link that shows exactly what a person scrolling Facebook or Instagram would see. Pass adAccountId + adId (from list_meta_ads), or creativeId. Optional placements (comma-separated): facebook_feed, facebook_feed_desktop, facebook_story, facebook_reels, facebook_profile_feed, facebook_marketplace, facebook_right_column, facebook_video_feed, instagram_feed, instagram_story, instagram_reels, instagram_explore, instagram_profile_feed, messenger_inbox, messenger_story, audience_network — default facebook_feed + instagram_feed + instagram_story + instagram_reels. Short words are read too and the reply says how: feed means facebook_feed, story/stories means facebook_story, reels means facebook_reels, instagram/ig means instagram_feed, ig_story, ig_reels. Free, read-only, spends nothing. THE LINKS EXPIRE AFTER 24 HOURS — always say so when handing them to a user. Use it straight after create_meta_ad, and whenever someone wants to approve an ad before it runs.
| Argument | Type | Description |
|---|---|---|
| adId | string | the ad to preview (from list_meta_ads) |
| creativeId | string | preview a creative directly instead of an ad |
| placements | string | comma-separated placements (see the list above) |
| adAccountId* | string | ad account id (act_… or digits) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"adId": {
"type": "string",
"description": "the ad to preview (from list_meta_ads)"
},
"creativeId": {
"type": "string",
"description": "preview a creative directly instead of an ad"
},
"placements": {
"type": "string",
"description": "comma-separated placements (see the list above)"
},
"adAccountId": {
"type": "string",
"description": "ad account id (act_… or digits)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId* | string | the ad to preview — list_openai_ads_campaigns with an adGroupId lists them |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adId"
],
"properties": {
"adId": {
"type": "string",
"description": "the ad to preview — list_openai_ads_campaigns with an adGroupId lists them"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| message | string | one user message to send; use messages for a full transcript |
| messages | array | full transcript as [{role, content}], in place of message |
| businessAgentId* | string | business agent id to chat with |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"businessAgentId"
],
"properties": {
"message": {
"type": "string",
"description": "one user message to send; use messages for a full transcript"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"required": [
"role",
"content"
],
"properties": {
"role": {
"type": "string",
"description": "speaker role, e.g. user or assistant"
},
"content": {
"type": "string",
"description": "text of this message"
}
}
},
"description": "full transcript as [{role, content}], in place of message"
},
"businessAgentId": {
"type": "string",
"description": "business agent id to chat with"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| pinId | string | existing promotable Pin id to preview |
| title | string | required with imageUrl |
| itemId | string | catalog item (product) id to preview within the product group |
| imageUrl | string | public image URL to preview before it is pinned; needs title |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| creativeType | string | pin: SHOPPING, COLLECTION, MAX_VIDEO, MAX_WIDTH_VIDEO_COLLECTION, MAX_WIDTH_REGULAR_COLLECTION; product group: SHOPPING, COLLECTION, CAROUSEL, MAX_WIDTH_COLLECTION |
| productGroupId | string | catalog product group id, for a shopping ad preview |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"pinId": {
"type": "string",
"description": "existing promotable Pin id to preview"
},
"title": {
"type": "string",
"description": "required with imageUrl"
},
"itemId": {
"type": "string",
"description": "catalog item (product) id to preview within the product group"
},
"imageUrl": {
"type": "string",
"description": "public image URL to preview before it is pinned; needs title"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"creativeType": {
"type": "string",
"description": "pin: SHOPPING, COLLECTION, MAX_VIDEO, MAX_WIDTH_VIDEO_COLLECTION, MAX_WIDTH_REGULAR_COLLECTION; product group: SHOPPING, COLLECTION, CAROUSEL, MAX_WIDTH_COLLECTION"
},
"productGroupId": {
"type": "string",
"description": "catalog product group id, for a shopping ad preview"
}
}
}See a Snapchat ad before it spends: a Snap-hosted preview page of the creative (no login, live for 90 days), its type and its review status. snapcode:true adds a Snapcode image to scan with the Snapchat app (its expiry is printed). Pass creativeId, or adId to preview the creative that ad uses. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id whose creative should be previewed |
| snapcode | boolean | true adds a Snapcode image to scan with the Snapchat app |
| creativeId | string | creative id to preview; or pass adId |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad id whose creative should be previewed"
},
"snapcode": {
"type": "boolean",
"description": "true adds a Snapcode image to scan with the Snapchat app"
},
"creativeId": {
"type": "string",
"description": "creative id to preview; or pass adId"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}WHAT WOULD HAPPEN IF YOU PUBLISHED — free, read-only, and it publishes nothing. Returns the container, the version live on the site right now and what it holds, the exact list of staged changes (added, updated, deleted) as Tag Manager itself reports them, any merge conflicts, and the one sentence to show a user before asking them to agree. Use it to answer 'what is waiting to go live?' without going anywhere near the publish call. publish_tag_manager_container without confirm returns the same facts, so this is for when you want them with no chance of a publish at all. 0 credits. Needs Google Tag Manager connected.
| Argument | Type | Description |
|---|---|---|
| workspace | string | older name for gtmWorkspace (MCP only) |
| versionPath | string | optional — preview re-publishing an EXISTING version instead of the workspace |
| gtmWorkspace | string | Tag Manager workspace id or name; defaults to the Default Workspace |
| containerPath* | string | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"containerPath"
],
"properties": {
"workspace": {
"type": "string",
"description": "older name for gtmWorkspace (MCP only)"
},
"versionPath": {
"type": "string",
"description": "optional — preview re-publishing an EXISTING version instead of the workspace"
},
"gtmWorkspace": {
"type": "string",
"description": "Tag Manager workspace id or name; defaults to the Default Workspace"
},
"containerPath": {
"type": "string",
"description": "the \"accounts/<id>/containers/<id>\" path from list_tag_manager_containers"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId | string | preview an existing ad |
| adText | string | 1-100 characters (video preview) |
| videoId | string | preview a video before building the ad (from upload_tiktok_ads_creative) |
| language | string | ENGLISH, CHINESE or JAPANESE; language of the preview |
| placement | string | existing ad only; default TikTok |
| displayName | string | the brand name shown on the ad, 1-40 characters (video preview) |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| callToAction | string | default LEARN_MORE (video preview) |
See X posts exactly as the ad will render — X returns a preview page per post (works for promoted-only posts). Read-only, free.
| Argument | Type | Description |
|---|---|---|
| tweetIds* | array | post ids to preview, from list_x_ads_posts |
| accountId | string | older name for adAccountId (MCP only) |
| tweetType | string | state of the posts: DRAFT, PUBLISHED or SCHEDULED |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"tweetIds"
],
"properties": {
"tweetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "post ids to preview, from list_x_ads_posts"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"tweetType": {
"enum": [
"DRAFT",
"PUBLISHED",
"SCHEDULED"
],
"type": "string",
"description": "state of the posts: DRAFT, PUBLISHED or SCHEDULED"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to publish; campaigns serve this version to real users |
| businessAgentId* | string | business agent id to publish |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"businessAgentId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to publish; campaigns serve this version to real users"
},
"businessAgentId": {
"type": "string",
"description": "business agent id to publish"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| leadFormId* | string | lead form id to publish |
| expectedDraftRevisionId* | string | draftRevisionId from the last read; refused if stale |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"leadFormId",
"expectedDraftRevisionId"
],
"properties": {
"leadFormId": {
"type": "string",
"description": "lead form id to publish"
},
"expectedDraftRevisionId": {
"type": "string",
"description": "draftRevisionId from the last read; refused if stale"
}
}
}PUBLISH THE CONTAINER — the ONE call in Hermoso that changes what runs on the customer's live website for every visitor, immediately. Everything else in Tag Manager is staged; this is not. TWO STEPS UNDER THE HOOD, because Tag Manager publishes a VERSION and not a workspace: Hermoso turns the draft workspace into a version and then publishes that version. CALLING IT WITHOUT confirm PUBLISHES NOTHING and is the correct first move: it returns what is live right now, how many tags, triggers and variables that live version holds, and exactly what would change — read back from Google, not echoed from your request. Show the user that, get an unambiguous yes, then call again. THE GATE SCALES WITH THE DAMAGE. A container that has NEVER been published has nothing that currently works and nothing this can break, so confirm:true alone is enough. A container that ALREADY has a live version is replacing something that works, so it additionally needs confirmPublicId set to that container's GTM-XXXXXXX public id — the id printed on the snippet installed on the website. It cannot be guessed; read it from list_tag_manager_containers or from the preview. IT REFUSES, FOR FREE AND WITHOUT PUBLISHING, in four cases you should relay rather than retry: merge conflicts in the workspace (somebody changed the same thing in the container — resolve in Tag Manager first), a workspace with no changes at all, a version Google reports a COMPILER ERROR in, and a container whose public id could not be read. TO UNDO A PUBLISH, publish the previous version again: pass versionPath (from list_tag_manager_versions) and no workspace is involved at all. The reply names the version that was live before this call in rolledBackTo for exactly that purpose, so a publish is reversible. READ verified BEFORE YOU TELL ANYONE IT IS LIVE. It is three-valued: true means the container's live version was re-read and IS the one just published; false means Google accepted it but something else is live, which can be Tag Manager's own read lag and must be reported as unconfirmed; null means the read-back could not run, so it is not known — never report false or null as published. TWO THINGS THAT SURPRISE PEOPLE: creating the version DELETES the draft workspace it was made from (Tag Manager's own behaviour) and opens a fresh empty one, whose id is in the reply, so any workspace id you were holding is stale afterwards. And publishing needs the PUBLISH right on the container in Tag Manager, Admin, User Management, which is separate from Edit — an account that can stage a tag can still be refused here, and that is a permission on the container, not a broken connection. 0 credits. Needs Google Tag Manager connected.
Read the ROWS of a Notion database the user shared with Hermoso — e.g. the team's content calendar: each row's title, id, link and every property as a plain value (status, date, channels, caption, links), plus the database's columns with their types and options. Filter with match {"Status": "Scheduled"} (typed from the database's own columns) or a raw Notion filter object; sort with sortBy (a column, or created_time / last_edited_time) and direction. Up to 100 rows per call, cursor for more; the answer says when Notion stopped at its 10,000-result limit. Use it to import a calendar and schedule its posts, or to find the row update_notion_row changes. Read-only, free. Needs Notion connected (Settings > Connectors > Notion).
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-100, default 50 |
| match | object | {"Column": value} — equality for status/select/checkbox/number/date, contains for text/multi-select |
| cursor | string | nextCursor from the previous page of rows |
| filter | object | a raw Notion filter object |
| sortBy | string | column name to sort by, or created_time or last_edited_time |
| archived | boolean | read archived rows instead |
| direction |
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.
| Argument | Type | Description |
|---|---|---|
| after | string | pagination cursor from the previous page |
| limit | number | max products per page |
| feedId* | string | product feed id to query |
| filters | array | product filters; same shape as an ad group's productSet |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"feedId"
],
"properties": {
"after": {
"type": "string",
"description": "pagination cursor from the previous page"
},
"limit": {
"type": "number",
"description": "max products per page"
},
"feedId": {
"type": "string",
"description": "product feed id to query"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"required": [
"field",
"operator",
"values"
],
"properties": {
"field": {
"type": "string",
"description": "product attribute name to filter on"
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "values the attribute is matched against"
},
"operator": {
"type": "string",
"description": "in, not_in, gt, gte, lt, lte, contains, not_contains or starts_with"
}
}
},
"description": "product filters; same shape as an ad group's productSet"
}
}
}Read the text of a Google Doc Hermoso can reach — one it created, or one the user handed over with the Google file picker in the app (that is how an EXISTING doc becomes readable; find its id with list_drive_files). Pass documentId (from create_doc) OR paste a Google Docs URL as docUrl. Under the drive.file scope it reaches nothing else in the user’s Drive; if Google answers that the file was not found, the user has not picked it yet — ask them to pick it in the app rather than retrying. Returns the plain text of EVERY tab (each headed by its tab name when there are several), with smart chips (dropdowns, people, dates, rich links) shown as the text Docs displays, plus `dropdowns[]` — each chip with its title, current value and options, which is what update_doc dropdowns sets. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| docUrl | string | a Google Docs URL to read — the document id is extracted from it |
| documentId | string | the document id (from create_doc) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"docUrl": {
"type": "string",
"description": "a Google Docs URL to read — the document id is extracted from it"
},
"documentId": {
"type": "string",
"description": "the document id (from create_doc)"
}
}
}Read the LEADS a form (formId) or a single ad (adId) has collected — the real answers people submitted, fetched live from Meta. THIS RETURNS REAL PEOPLE’S CONTACT DETAILS (names, emails, phone numbers). Treat it as the user’s own customer data: show only what they asked for, never post a lead list anywhere public, and do not copy it into an unrelated document. Hermoso PULLS these on demand and keeps no copy — nothing here watches, polls or files leads anywhere, so if the user wants this batch kept, put it somewhere THEY own in the same turn (a Google Sheet with create_sheet / append_to_sheet, a doc, or their own CRM). Meta makes each lead available for 90 days after it is submitted; anything older is handled in Meta’s own Leads Center and CRM integrations. Narrow with since/until (ISO dates), page with cursor, or pass redact:true to see counts, timestamps and which ad produced each lead WITHOUT the personal details. NEEDS leads_retrieval — see list_meta_lead_forms for what to do if Meta refuses it.
| Argument | Type | Description |
|---|---|---|
| adId | string | just the leads this ONE ad produced (from list_meta_ads) — pass this OR formId, never both |
| limit | number | 1–500, default 100 |
| since | string | ISO date (2026-08-01) or epoch ms — only leads created after this |
| until | string | ISO date or epoch ms — only leads created before this |
| cursor | string | paging cursor returned by a previous call |
Read a Notion page the user shared with Hermoso, as markdown (pageId or a Notion link). A database id answers with its data sources instead. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| pageId* | string | page id or Notion link |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pageId"
],
"properties": {
"pageId": {
"type": "string",
"description": "page id or Notion link"
}
}
}Read cells from a Google Sheet Hermoso can reach — one it created, or one the user handed over with the Google file picker in the app (that is how an EXISTING spreadsheet becomes readable; find its id with list_drive_files). Pass the spreadsheetId (from create_sheet) OR paste a Google Sheets URL as sheetUrl. If Google answers that the file was not found, the user has not picked it yet — ask them to pick it in the app rather than retrying. Returns a 2-D array of values.
| Argument | Type | Description |
|---|---|---|
| range | string | A1 range, e.g. "A1:D50" (default A1:Z1000) |
| sheetUrl | string | a Google Sheets URL to read — the spreadsheet id is extracted from it |
| spreadsheetId | string | the spreadsheet id (from create_sheet) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"range": {
"type": "string",
"description": "A1 range, e.g. \"A1:D50\" (default A1:Z1000)"
},
"sheetUrl": {
"type": "string",
"description": "a Google Sheets URL to read — the spreadsheet id is extracted from it"
},
"spreadsheetId": {
"type": "string",
"description": "the spreadsheet id (from create_sheet)"
}
}
}Record an ad or campaign the merchant is running (on Meta, Google, TikTok, email, anywhere) in their Shopify admin under Marketing, so Shopify's own reports attribute storefront visits and sales to it by its UTM. REPORTING ONLY: it publishes nothing and spends nothing. Pass title, remoteUrl (where the ad is managed, e.g. the Ads Manager link), the utm {campaign, source, medium} the ad's links carry, and optionally tactic (AD default, RETARGETING, POST, LINK, NEWSLETTER, …), channel (SOCIAL default, SEARCH, DISPLAY, EMAIL, REFERRAL), status (ACTIVE default, PAUSED, SCHEDULED, INACTIVE, DELETED_EXTERNALLY), budget {type DAILY|LIFETIME, amount, currency}, adSpend {amount, currency}, start / end / scheduledEnd dates and previewImageUrl. remoteId is YOUR stable id for the activity (the ad platform's campaign id is ideal); calling again with the same remoteId UPDATES that one row, and every field you leave out is KEPT from the existing record (Shopify's own upsert would erase it; the reply lists what was kept). Read back from Shopify. Needs the store to have approved Hermoso's marketing permission: if it has not, the reply says to open Hermoso once from the Shopify admin to approve it. Only works for accounts created by installing Hermoso from the Shopify App Store. Free.
| Argument | Type | Description |
|---|---|---|
| end | string | ISO date it ended |
| utm | object | the UTM the ad links carry; Shopify matches visits on all three |
| start | string | ISO date the ad started |
| title | string | what the merchant sees in Shopify > Marketing; required on the first recording |
| budget | object |
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.
| Argument | Type | Description |
|---|---|---|
| budget* | number | budget in the ad account’s currency |
| bidType | string | default CPC — must fit the campaign objective |
| endTime | string | ISO 8601 end of the flight to price |
| currency | string | currency code, e.g. USD; returned amounts use the ad account currency |
| goalType | string | DAILY_SPEND or LIFETIME_SPEND |
| objective | string | default CLICKS |
| startTime | string | ISO 8601 start of the flight to price |
| targeting | object | targeting block, same shape as create_reddit_ads_ad_group targeting |
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.
| Argument | Type | Description |
|---|---|---|
| jobId | string | check an existing job instead of creating one |
| confirm | boolean | true only after the user approved — deletion is irreversible |
| pixelId | string | limit the deletion to one pixel’s data (from list_reddit_ads_pixels) |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| identifiers | array | who to delete; omit when checking a jobId |
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.
| Argument | Type | Description |
|---|---|---|
| budget* | number | budget in the ad account’s currency (not micro-currency — the conversion is handled) |
| bidType | string | CPC, CPM, CPV, CPV6 or CPV15; must fit the objective |
| endTime | string | ISO 8601 end of the forecast window |
| goalType | string | DAILY_SPEND or LIFETIME_SPEND; LIFETIME_SPEND needs endTime |
| bidAmount | number | bid in the ad account currency; needed for paid bid types |
| objective | string | default CLICKS |
| startTime | string | ISO 8601 |
| targeting |
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.
| Argument | Type | Description |
|---|---|---|
| limit | number | default 50, max 200 |
| since | string | YYYY-MM-DD or full ISO timestamp |
| until | string | YYYY-MM-DD or full ISO timestamp |
| entityIds | array | restrict to these objects |
| memberIds | array | restrict to changes made by these Reddit members |
| entityType | string | required when you pass entityIds |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| changeTypes |
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.
| Argument | Type | Description |
|---|---|---|
| budget* | number | budget in the ad account’s currency (not micro-currency) |
| bidType | string | CPC, CPM, CPV, CPV6 or CPV15 |
| endTime | string | ISO 8601; default 30 days after the start, at most 365 |
| goalType* | string | whether budget is per day or for the whole plan |
| bidAmount | number | bid in the ad account currency |
| objective* | string | campaign objective |
| startTime | string | ISO 8601; default now |
| targeting |
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.
| Argument | Type | Description |
|---|---|---|
| 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 | array | 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. |
| filter | string | Reddit filter expression to restrict rows to particular ids/values |
Refund a Stripe charge (ch_… or a pi_… payment intent): the full refundable amount, or a partial `amount` in major units; optional reason duplicate / fraudulent / requested_by_customer. ALWAYS needs confirm:true, test keys included. Read back. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| amount | number | partial refund in major units; omit to refund the full refundable amount |
| reason | string | duplicate, fraudulent or requested_by_customer |
| confirm | boolean | true after the user approves this refund |
| chargeId* | string | charge id, ch_..., or payment intent id, pi_... |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"chargeId"
],
"properties": {
"amount": {
"type": "number",
"description": "partial refund in major units; omit to refund the full refundable amount"
},
"reason": {
"type": "string",
"description": "duplicate, fraudulent or requested_by_customer"
},
"confirm": {
"type": "boolean",
"description": "true after the user approves this refund"
},
"chargeId": {
"type": "string",
"description": "charge id, ch_..., or payment intent id, pi_..."
}
}
}ONE-TIME SETUP, and the FIRST thing to run when Merchant Center calls are being refused. Google blocks every Merchant API call until Hermoso's Google Cloud project is registered against the merchant's account, and says so verbatim ("GCP project ... is not registered with the merchant account"). This performs that link. It is NOT a broken connection and reconnecting cannot fix it. Google asks for up to 5 minutes afterwards before the API starts answering.
| Argument | Type | Description |
|---|---|---|
| developerEmail* | string | a contact address Google records against the registration; it is not used to sign in |
| merchantCenterId* | string | the numeric id shown top-right in Merchant Center |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"developerEmail"
],
"properties": {
"developerEmail": {
"type": "string",
"description": "a contact address Google records against the registration; it is not used to sign in"
},
"merchantCenterId": {
"type": "string",
"description": "the numeric id shown top-right in Merchant Center"
}
}
}Save a durable fact or PREFERENCE about the brand, audience, or the user’s creative TASTE (e.g. “audience is first-time homebuyers”, “prefers bold lime accents”, “always captions off”) into the workspace Memory so it shapes FUTURE ads. For lasting things, not one-off requests. Merges into the existing Memory (never overwrites); de-dupes on identical text. NEVER how Hermoso, a tool, a connector or a platform API behaves (what a call returns, errors, permissions, limits, ids) and never a phone number or email — those are refused.
| Argument | Type | Description |
|---|---|---|
| text* | string | the fact/preference, concise |
| brand | string | profile id/name from list_brands, this call only |
| category | string | short bucket: Brand, Audience, Taste, Do, Don’t, or Preference (default General) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "the fact/preference, concise"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"category": {
"type": "string",
"description": "short bucket: Brand, Audience, Taste, Do, Don’t, or Preference (default General)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true to remove, after a first call without it has shown the campaigns |
| campaignIds | array | only these campaigns (up to 20); omit for all |
| confirmName | string | its exact name, when asked |
| creativeSetId* | string | AppLovin creative set to take out of its campaigns |
| confirmChildren | number | the campaign count the unconfirmed call reported |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"creativeSetId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "true to remove, after a first call without it has shown the campaigns"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "only these campaigns (up to 20); omit for all"
},
"confirmName": {
"type": "string",
"description": "its exact name, when asked"
},
"creativeSetId": {
"type": "string",
"description": "AppLovin creative set to take out of its campaigns"
},
"confirmChildren": {
"type": "number",
"description": "the campaign count the unconfirmed call reported"
}
}
}Remove a site from the connected Bing Webmaster account — the heaviest thing in this connector. GATED ON BLAST RADIUS: called WITHOUT `confirm` it removes nothing and reports whether Bing holds the site as VERIFIED and how many sitemaps are registered on it; a site that is verified or has any sitemaps then ALSO needs `confirmSiteUrl` set to that exact site string, which is a fact you only have after inspecting it. A site that was never verified and has no sitemaps is the empty radius and stays a one-call removal — friction that does not scale with the loss just gets routed around. RE-ADDING A VERIFIED SITE LATER MEANS PROVING OWNERSHIP FROM SCRATCH; no API restores it. Nothing about the site itself or its Bing ranking changes. The site is also dropped from this brand's shared list, and if the read-back that confirms the removal cannot run the answer says "could not tell", never "removed". 0 credits.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true; without it nothing is removed and you get the radius instead |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
| confirmSiteUrl | string | the exact site string again — required unless the site is unverified with no sitemaps |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true; without it nothing is removed and you get the radius instead"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
},
"confirmSiteUrl": {
"type": "string",
"description": "the exact site string again — required unless the site is unverified with no sitemaps"
}
}
}Remove a sitemap from Bing. GATED ON BLAST RADIUS, not just on intent: called WITHOUT `confirm` it removes nothing and reports what Bing says that sitemap actually carries — its URL count, status and type — and a sitemap carrying any URLs then ALSO needs `confirmUrlCount` set to that number. confirm:true proves you meant to remove something; the echoed count proves you aimed at the sitemap you inspected rather than one that merely shares a name. A sitemap Bing reports as empty stays a one-call removal. Removing it does NOT remove those pages from Bing's index — Bing simply stops using it to find new ones — and it can be submitted again at any time. If the read-back that confirms the removal cannot run, the answer says "could not tell", never "removed". 0 credits.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true; without it nothing is removed and you get the radius instead |
| feedUrl* | string | the FULL sitemap URL to remove, exactly as list_bing_webmaster_sitemaps shows it |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
| confirmUrlCount | number | the URL count Bing reports for that sitemap — required when it carries any |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"feedUrl"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true; without it nothing is removed and you get the radius instead"
},
"feedUrl": {
"type": "string",
"description": "the FULL sitemap URL to remove, exactly as list_bing_webmaster_sitemaps shows it"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
},
"confirmUrlCount": {
"type": "number",
"description": "the URL count Bing reports for that sitemap — required when it carries any"
}
}
}Remove a member from this profile by email — they lose access (you can re-invite them later), AND every connection THEY made on this profile is disconnected with them: their own X, LinkedIn, TikTok, YouTube, Pinterest, Threads… connections (on a channel holding several accounts, only the accounts they added), and any extra login or authorization they added. Connections the owner or anyone else made are never touched, and one with no record of who connected it is treated as the owner's. Queued posts that would publish through their accounts, or that they scheduled themselves, will not go out. The unconfirmed call reports exactly which connections and how many scheduled posts — relay that to the user, then call with confirm:true.
| Argument | Type | Description |
|---|---|---|
| email* | string | the member’s email |
| confirm | boolean | REQUIRED true |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email"
],
"properties": {
"email": {
"type": "string",
"description": "the member’s email"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true on a LIVE campaign |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId | string | ad group to remove negatives from; pass this or campaignId |
| campaignId | string | campaign to remove negatives from; pass this or adGroupId |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| negativeKeywordIds* | array | ids of the negative keywords to remove, from list_microsoft_ads_negative_keywords |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"negativeKeywordIds"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true on a LIVE campaign"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "ad group to remove negatives from; pass this or campaignId"
},
"campaignId": {
"type": "string",
"description": "campaign to remove negatives from; pass this or adGroupId"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"negativeKeywordIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ids of the negative keywords to remove, from list_microsoft_ads_negative_keywords"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true removes the limit; needs expectedRevision as well |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"confirm": {
"type": "boolean",
"description": "true removes the limit; needs expectedRevision as well"
},
"windowId": {
"type": "string",
"description": "the daily rule to remove; required when the account has more than one"
},
"expectedRevision": {
"type": "number",
"description": "the revision from list_openai_ads_spend_windows"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | new asset name |
| assetId* | string | id of the asset to rename |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"assetId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "new asset name"
},
"assetId": {
"type": "string",
"description": "id of the asset to rename"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}Post a public reply, as the brand, under a comment on one of your TikTok ads. THE REPLY IS LIVE IMMEDIATELY AND IS PUBLIC, posted as the TikTok identity you name, so let the user approve the wording. TikTok requires FIVE ids and every one of them is on the comment row itself — pass back commentId, adId, tiktokItemId, identityId and identityType exactly as list_tiktok_ads_comments reported them rather than assembling them by hand; a mismatched identity is a public mistake. IdentityType on the comment endpoints is narrower than on the ad builder: only TT_USER or CUSTOMIZED_USER. The reply comes back as TikTok’s own stored row with its new comment id, so the answer is TikTok’s record and not an echo — and because it is your own comment, delete_tiktok_ads_comment can remove it.
| Argument | Type | Description |
|---|---|---|
| adId* | string | from the same comment row |
| text* | string | the reply, exactly as it will appear in public |
| commentId* | string | the comment being replied to — from list_tiktok_ads_comments |
| identityId* | string | from the same comment row (or list_tiktok_ads_identities) — the account the reply is posted AS |
| advertiserId | string | from list_tiktok_ads_accounts |
| identityType* | string | as reported on the comment row |
Report a bug in Hermoso to the team. Use this when something in Hermoso genuinely misbehaves — a tool errors unexpectedly, returns a wrong or malformed result, a render comes back broken, or documented behaviour doesn't match what happened. Include what you were trying to do, the exact tool call and arguments, and what came back. Do NOT use it for out-of-credits, a policy refusal, or a missing capability (use request_feature for that). Free, no credits.
| Argument | Type | Description |
|---|---|---|
| details* | string | what you were doing, the tool + arguments you called, what you expected, and what actually happened (paste the exact error) |
| summary* | string | one-line summary of the bug |
| severity | string | high = blocks the task or loses paid work; medium = wrong output but workable; low = cosmetic |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"summary",
"details"
],
"properties": {
"details": {
"type": "string",
"description": "what you were doing, the tool + arguments you called, what you expected, and what actually happened (paste the exact error)"
},
"summary": {
"type": "string",
"description": "one-line summary of the bug"
},
"severity": {
"enum": [
"low",
"medium",
"high"
],
"type": "string",
"description": "high = blocks the task or loses paid work; medium = wrong output but workable; low = cosmetic"
}
}
}Ask the Hermoso team for a capability that doesn't exist yet. Use this when you need something Hermoso genuinely can't do — an unsupported platform or channel, a missing model, an export format, a tool that would have completed the user's task but isn't available. Say what the user was trying to achieve, not just the feature name — the use case is what gets built. Free, no credits.
| Argument | Type | Description |
|---|---|---|
| details* | string | what the user was actually trying to achieve, why the current tools couldn't do it, and what you'd expect the capability to do |
| summary* | string | one line: the capability you need |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"summary",
"details"
],
"properties": {
"details": {
"type": "string",
"description": "what the user was actually trying to achieve, why the current tools couldn't do it, and what you'd expect the capability to do"
},
"summary": {
"type": "string",
"description": "one line: the capability you need"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| action | string | EXTEND adds the days to an authorization already approved |
| videoId* | string | a video the creator has already linked to the campaign |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
| authorizationDays | number | 1-365, TikTok defaults to 30: a REQUEST, not a guarantee |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"videoId"
],
"properties": {
"action": {
"enum": [
"EXTEND"
],
"type": "string",
"description": "EXTEND adds the days to an authorization already approved"
},
"videoId": {
"type": "string",
"description": "a video the creator has already linked to the campaign"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
},
"authorizationDays": {
"type": "number",
"description": "1-365, TikTok defaults to 30: a REQUEST, not a guarantee"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| audienceId* | string | audience the operation belongs to |
| operationId* | string | operation id of the stopped add or remove |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId",
"operationId"
],
"properties": {
"audienceId": {
"type": "string",
"description": "audience the operation belongs to"
},
"operationId": {
"type": "string",
"description": "operation id of the stopped add or remove"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| endDate | string | YYYY-MM-DD |
| startDate | string | YYYY-MM-DD |
| templateId* | string | the template id |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| granularity | string | default the template’s own |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"templateId"
],
"properties": {
"endDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"templateId": {
"type": "string",
"description": "the template id"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"granularity": {
"enum": [
"TOTAL",
"DAY",
"HOUR",
"WEEK",
"MONTH"
],
"type": "string",
"description": "default the template’s own"
}
}
}Add a portrait to this workspace’s reusable CAST so the SAME person can star in future ads — the headless twin of the app’s + > Pick a creator > save. Pass the portrait’s public url plus a name to call them by. Saving is FREE and renders nothing. LIKENESS: leave `source` "generated" for an AI-made person (free on every plan) and use "upload"/"social" for a REAL person. Using a real person’s face, likeness or voice confirms you are them or have their consent, take full, unlimited responsibility for its use and will cover any claim against Hermoso (hermoso.ai/terms). Paid plan only; recorded.
| Argument | Type | Description |
|---|---|---|
| look | string | their canonical wardrobe/appearance in words — reused to hold the look steady across ads |
| name* | string | what to call this creator (e.g. “Sarah”) — list_creators and the app’s picker match on it |
| brand | string | profile id/name from list_brands, this call only |
| image | string | REQUIRED except with useAnyway. public https url of the portrait (an existing render’s url, or any public photo). Not a local file path — upload it with upload_file first and save the url that returns |
| poses | array | up to 4 extra full-body / angle plates of the SAME person (public urls) — they make a wider shot hold the identity |
| voice | string | a default voice name for this persona (engines + voices are in hermoso_capabilities) |
Save a reusable PLAYBOOK — the strategy takeaways worth re-running: the hooks that work, the angles, the formats, and the concrete plays. Lands in the same Playbooks library the web app lists, runs and manages. Distinct from save_skill (a directive applied to every ad) and from the swipefile (raw saved creative). Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | the playbook headline — what it is, in a few words |
| brand | string | profile id/name from list_brands, this call only |
| hooks | array | the opening hooks worth reusing, verbatim |
| plays | array | the concrete plays to run ({title, detail}) — the actionable half |
| angles | array | the persuasion angles ({title, detail}) |
| source | string | where it came from, e.g. “teardown · Ridge” |
| formats | array | the formats/recipes this plays best in (e.g. ugc_selfie, cinematic, static) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the playbook headline — what it is, in a few words"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"hooks": {
"type": "array",
"items": {
"type": "string"
},
"description": "the opening hooks worth reusing, verbatim"
},
"plays": {
"type": "array",
"items": {
"type": "object",
"required": [
"title"
],
"properties": {
"title": {
"type": "string",
"description": "the play to run, in a few words"
},
"detail": {
"type": "string",
"description": "how to run the play; optional, up to 600 characters"
}
},
"additionalProperties": {}
},
"description": "the concrete plays to run ({title, detail}) — the actionable half"
},
"angles": {
"type": "array",
"items": {
"type": "object",
"required": [
"title"
],
"properties": {
"title": {
"type": "string",
"description": "the angle, in a few words"
},
"detail": {
"type": "string",
"description": "what the angle is and why it persuades; optional, up to 600 characters"
}
},
"additionalProperties": {}
},
"description": "the persuasion angles ({title, detail})"
},
"source": {
"type": "string",
"description": "where it came from, e.g. “teardown · Ridge”"
},
"formats": {
"type": "array",
"items": {
"type": "string"
},
"description": "the formats/recipes this plays best in (e.g. ugc_selfie, cinematic, static)"
}
}
}Save a reusable custom SKILL — a named creative directive/playbook applied to future ads (a hook formula, a UGC recipe, a compliance rule, a named specialist persona like “our founder-story style” or “short-form ad strategist”). Distill an imperative, self-contained directive. Merges into the workspace Skills library (list_skills shows built-ins + your custom skills).
| Argument | Type | Description |
|---|---|---|
| name* | string | short skill name, e.g. “Founder-story hook” |
| brand | string | profile id/name from list_brands, this call only |
| directive* | string | the full instruction the skill applies when used (1–6 sentences, imperative) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"directive"
],
"properties": {
"name": {
"type": "string",
"description": "short skill name, e.g. “Founder-story hook”"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"directive": {
"type": "string",
"description": "the full instruction the skill applies when used (1–6 sentences, imperative)"
}
}
}Save a Hermoso render — or ANY file — into the user’s connected Google Drive. Pass a Hermoso render URL as url (or urls[] for several); for a local/external file, call upload_file first and pass the url it returns. Optional folder (created if new) + name. Returns the Drive file(s) with a webViewLink. Needs Google Drive connected (Settings > Connectors > Google Drive — one connection covers Drive, Sheets and Docs). NOTE: Hermoso uses the drive.file scope, so it reaches ONLY the files it created plus any the user explicitly handed over with the Google file picker in the app — never their whole Drive.
| Argument | Type | Description |
|---|---|---|
| url | string | a single Hermoso render URL to save |
| name | string | file name (single save) |
| urls | array | several render URLs (up to 20) to save in one call |
| folder | string | Drive folder name to save into (created if new) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "a single Hermoso render URL to save"
},
"name": {
"type": "string",
"description": "file name (single save)"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "several render URLs (up to 20) to save in one call"
},
"folder": {
"type": "string",
"description": "Drive folder name to save into (created if new)"
}
}
}Save to the user's Notion as a NEW page: a research report, competitor teardown or campaign report as `content` (markdown: headings, lists, links, and an image or video URL on its own line becomes the media), a swipefile collection (swipefileCollection: name or id; each ad with its creative, copy, run dates and link), the content calendar (includeSchedule:true), or a mix. Put it under parentPageId, as a row in databaseId, or omit both for a private page in their workspace. Long text is split across blocks, never cut. The answer is read back from Notion. Free; never overwrites anything.
| Argument | Type | Description |
|---|---|---|
| icon | string | one emoji |
| title | string | title of the new Notion page |
| content | string | markdown body |
| databaseId | string | database id or link: saves as a new row |
| dataSourceId | string | data source id to save into as a new row; pass only one of parentPageId, databaseId, dataSourceId |
| parentPageId | string | page id or link to save under |
| includeSchedule | boolean | add the scheduled posts |
| swipefileCollection | string | swipefile collection name or id |
Save a Hermoso render — or ANY file — into the user’s connected Microsoft OneDrive. Pass a Hermoso render URL as url (or urls[] for several); for a local/external file, call upload_file first and pass the url it returns. Optional folder (created if new) + name. Returns the OneDrive file(s) with a webViewLink. Needs OneDrive connected (Settings > Connectors > OneDrive).
| Argument | Type | Description |
|---|---|---|
| url | string | a single Hermoso render URL to save |
| name | string | file name (single save) |
| urls | array | several render URLs (up to 20) to save in one call |
| folder | string | OneDrive folder name to save into (created if new) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "a single Hermoso render URL to save"
},
"name": {
"type": "string",
"description": "file name (single save)"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "several render URLs (up to 20) to save in one call"
},
"folder": {
"type": "string",
"description": "OneDrive folder name to save into (created if new)"
}
}
}Save one or more ads/creatives to a named SWIPEFILE collection, creating the collection if it does not exist — the headless twin of the heart on every ad card in the web app. Saved ads persist to the workspace board the web Swipefile tab shows, and feed the taste signal every future ad is planned against. De-dupes: re-saving the same ad (same key/link/media) MOVES it into the named collection instead of duplicating it. Free.
| Argument | Type | Description |
|---|---|---|
| brand | string | profile id/name from list_brands, this call only |
| items* | array | the ads to save |
| collection* | string | the collection name — an existing one, or a new one to create |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"collection",
"items"
],
"properties": {
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "a stable id for this ad if you have one (an ad_archive_id, creativeId, …). Omit and one is derived from the link/media so re-saving is idempotent"
},
"body": {
"type": "string",
"description": "the ad copy"
},
"link": {
"type": "string",
"description": "link to the ad in its library / the destination URL"
},
"image": {
"type": "string",
"description": "image URL"
},
"title": {
"type": "string",
"description": "headline / hook"
},
"video": {
"type": "string",
"description": "video URL"
},
"pageName": {
"type": "string",
"description": "alias of advertiser"
},
"platform": {
"type": "string",
"description": "where it ran — 'meta', 'google', 'linkedin', 'tiktok', 'generated', …"
},
"page_name": {
"type": "string",
"description": "alias of advertiser: the field search_meta_ads returns, accepted as-is"
},
"advertiser": {
"type": "string",
"description": "the brand running the ad"
}
},
"additionalProperties": {}
},
"minItems": 1,
"description": "the ads to save"
},
"collection": {
"type": "string",
"description": "the collection name — an existing one, or a new one to create"
}
}
}Find the App Store app to advertise and its adamId — the id every other Apple Ads tool needs (create_apple_ads_campaign’s appAdamId, a creative’s destination, an eligibility check). Call it with NO arguments to list the apps THIS Apple Ads organization owns, which is what a first-time advertiser wants: only an app you own can be promoted, so a public app you merely found by name cannot be used as promotedObjectId. Pass query to search the App Store catalogue by name or developer, or adamId to fetch one app’s full details — including availableStorefronts, the definitive list of countries the app can be advertised in and the set a campaign’s countries must be a subset of. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| cpids | string | Comma-separated iTunes content provider ids to scope the search to. |
| limit | number | maximum number of apps to return |
| query | string | App name or developer to search the App Store for. |
| adamId | string | Fetch one app’s full details instead of searching. |
| offset | number | apps to skip, for paging |
| ownedOnly | boolean | Default true — only apps this organization owns (the only ones that can be promoted). false searches the whole catalogue. Apple’s own default is false; Hermoso flips it because only an owned app can be advertised. |
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.
| Argument | Type | Description |
|---|---|---|
| geos | array | 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 |
| query | string | Name to search, minimum 2 characters. "*" returns everything at the entity level. |
| entity | string | Narrow to one granularity. |
| offset | number |
THE REAL SEARCH TERMS — Google Search Console's performance report, and the most useful research tool here for writing copy. `dimensions:["query"]` returns the literal words people typed to reach this brand, with impressions, clicks, CTR and average position; `dimensions:["page","query"]` answers "which landing page already wins this demand", i.e. Where paid traffic should point. Every other dimension is available too: country, device, searchAppearance, date, hour. This is first-party MEASURED demand language, not a competitor's guess (an ad library) and not a model (a keyword planner), and it exists nowhere else: organic search terms left Google Analytics with "not provided" years ago. StartDate and endDate are REQUIRED and are exact YYYY-MM-DD in Pacific time — Google Analytics' relative forms like "28daysAgo" are NOT accepted anywhere in Search Console. THREE THINGS TO REPEAT WHENEVER YOU REPORT THESE NUMBERS: (1) rows are the TOP ones, not all of them, by Google's own statement; (2) anonymised queries are WITHHELD, so clicks summed per query will not equal the property total and that gap is not an error; (3) the default dataState "final" lags roughly 2-3 days, and "all" returns fresh partial data that will still change. Search Console keeps 16 months of history. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| type | string | web (default), image, video, news, discover or googleNews |
| endDate* | string | YYYY-MM-DD, required, Pacific time, inclusive |
| siteUrl* | string | the exact property string from list_search_console_sites — "sc-domain:example.com" or "https://example.com/", never a bare domain, and it must be one SHARED with this profile |
THE LIVE INDEX STATUS OF ONE PAGE, straight from Google: the index verdict, coverage state, whether robots.txt allows it, which URL Google picked as canonical versus the one the page itself declares, when it was last crawled and as which crawler, plus rich-result and mobile-usability verdicts. This is the tool for "why is this page getting no traffic" — a page Google cannot crawl, or whose canonical Google has reassigned to a different URL, will never rank however good the copy is. THE URL MUST SIT INSIDE THE PROPERTY: a URL-prefix property covers only its own prefix, while an "sc-domain:" property covers every scheme and subdomain. Important: QUOTA — Google allows 2,000 inspections per DAY and 600 per minute PER SITE, and that is the CUSTOMER'S property quota, not ours: inspect the handful of URLs the question is actually about and NEVER loop this over a sitemap, a page list or a crawl. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| siteUrl* | string | the exact property string from list_search_console_sites, and it must be one SHARED with this profile |
| languageCode | string | BCP-47 code for translated issue messages, e.g. "de-CH". Default en-US. |
| inspectionUrl* | string | the full URL to inspect, e.g. "https://example.com/pricing" — it must be under the property |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"inspectionUrl"
],
"properties": {
"siteUrl": {
"type": "string",
"description": "the exact property string from list_search_console_sites, and it must be one SHARED with this profile"
},
"languageCode": {
"type": "string",
"description": "BCP-47 code for translated issue messages, e.g. \"de-CH\". Default en-US."
},
"inspectionUrl": {
"type": "string",
"description": "the full URL to inspect, e.g. \"https://example.com/pricing\" — it must be under the property"
}
}
}Look up LinkedIn TARGETING entities by name and get their URNs — locations, job titles, industries, seniorities, company sizes, skills, job functions, interests, employers, degrees, fields of study, member behaviours. LinkedIn’s targeting values are opaque URNs (urn:li:geo:103644278 is the United States) with no guessable form, so ALWAYS resolve an audience here before passing it to create_linkedin_ads_campaign, and NEVER invent a URN — a made-up one either 400s or, worse, targets somebody else. If nothing matches, say so plainly. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| facet | string | facet name, default "locations" — e.g. locations, titles, industries, seniorities, staffCountRanges, skills, jobFunctions, interests, employers, degrees, fieldsOfStudy, memberBehaviors |
| query* | string | the name to search for, e.g. "United States", "Software Engineer", "Marketing" |
| country | string | default US |
| language | string | default en |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"facet": {
"type": "string",
"description": "facet name, default \"locations\" — e.g. locations, titles, industries, seniorities, staffCountRanges, skills, jobFunctions, interests, employers, degrees, fieldsOfStudy, memberBehaviors"
},
"query": {
"type": "string",
"description": "the name to search for, e.g. \"United States\", \"Software Engineer\", \"Marketing\""
},
"country": {
"type": "string",
"description": "default US"
},
"language": {
"type": "string",
"description": "default en"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | 1-100, default 25 |
| query* | string | part of the name to find (3+ characters for a company) |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| profileType* | string | what to search: CompanyName, Industry or JobFunction |
| languageLocale | string | en (default), fr, de, it, pt-BR, es or zh-Hant |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"profileType",
"query"
],
"properties": {
"limit": {
"type": "number",
"description": "1-100, default 25"
},
"query": {
"type": "string",
"description": "part of the name to find (3+ characters for a company)"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"profileType": {
"enum": [
"CompanyName",
"Industry",
"JobFunction"
],
"type": "string",
"description": "what to search: CompanyName, Industry or JobFunction"
},
"languageLocale": {
"type": "string",
"description": "en (default), fr, de, it, pt-BR, es or zh-Hant"
}
}
}THE IDS AN AD GROUP’S targetingSpec NEEDS. create_pinterest_ads_campaign and create_pinterest_ads_ad_group accept interests, locales, locations, age buckets and keywords — this is where those values come from, and guessing one is the worst option available: Pinterest ACCEPTS a well-formed but wrong interest id and the campaign then quietly targets the wrong people. Pass targetingType (INTEREST, GEO, LOCATION, LOCALE, AGE_BUCKET, GENDER, APPTYPE, KEYWORD, AUDIENCE_INCLUDE, AUDIENCE_EXCLUDE) and optionally `query` to narrow the list. INTERESTS ARE A TREE: pass interestId instead to drill into one and get its children, which is how you get from "Food" to something specific enough to target. Pinterest publishes no search parameter of its own, so `query` filters the full list HERE — the note says so, because a filter we applied is not a filter the vendor applied. Read-only, 0 credits, no new permission (it uses ads:read, already granted).
| Argument | Type | Description |
|---|---|---|
| limit | number | 1–500, default 100 |
| query | string | narrow the list to options mentioning this text (filtered here, not by Pinterest) |
| interestId | string | drill INTO one interest and return its child interests — how you walk the interest tree |
| adAccountId | string | Pinterest ad account id; omit when only one is shared with this profile |
| targetingType | string | which catalog to list — required unless you pass interestId |
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.
| Argument | Type | Description |
|---|---|---|
| kind | string | default communities |
| limit | number | max results, default 15 |
| query | string | keyword — required for communities; topics for community_suggestions; seeds for keywords; filters interests / devices / carriers / languages; searches cities for geolocations |
| country | string |
Resolve Snapchat targeting options to the ids an ad squad needs — countries (REQUIRED: an ad squad cannot be created without at least one), regions, metros, age groups, genders, languages, device OS or interests. NEVER invent one of these ids: invented targeting is silent and spends money on the wrong people. Region and metro lookups need a countryCode because Snapchat scopes those lists per country. Interests live in SEVERAL taxonomies at different paths (scls, vac, shp) which are not interchangeable, so name one rather than assuming. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| kind | string | default country |
| limit | number | maximum number of options to return |
| query | string | filter the returned list to matching entries |
| taxonomy | string | for kind:interest — scls, vac or shp |
| countryCode | string | REQUIRED for region and metro — two-letter code, e.g. "us" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"country",
"region",
"metro",
"age_group",
"gender",
"language",
"os_type",
"interest"
],
"type": "string",
"description": "default country"
},
"limit": {
"type": "number",
"description": "maximum number of options to return"
},
"query": {
"type": "string",
"description": "filter the returned list to matching entries"
},
"taxonomy": {
"type": "string",
"description": "for kind:interest — scls, vac or shp"
},
"countryCode": {
"type": "string",
"description": "REQUIRED for region and metro — two-letter code, e.g. \"us\""
}
}
}Look up the exact ids TikTok ad-group targeting expects, so none of them has to be invented. kind:"location" resolves TikTok’s targetable regions — an ad group CANNOT be created without location ids, TikTok refuses it in its own words ("‘location_ids’ or ‘zipcode_ids’ must be specified"). kind:"interest" resolves the interest categories and kind:"interest_keyword" the additional interest KEYWORDS (both attach to an ad group). kind:"hashtag" resolves real targeting HASHTAGS — until 2026-08-11 this kind pointed at TikTok’s interest-keyword endpoint and quietly returned interest categories instead; note that hashtag ids feed TikTok’s actions[] field, which Hermoso does not send yet, so treat hashtag results as RESEARCH rather than targeting you can apply. kind:"language" the language codes. A made-up id either fails the create or, worse, targets somebody else and spends money silently, so always resolve here first and never guess. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| kind | string | default location. interest_keyword and hashtag both REQUIRE keyword. |
| level | string | location only: return one level |
| limit | number | rows to return, default 50, max 200; total always counts every match |
| keyword | string | narrows the lookup by name, e.g. "Canada" returns Canada rather than every place TikTok has; REQUIRED for interest_keyword and hashtag |
| regionCode | string | location only: two-letter country code, e.g. CA, to list the places inside one country |
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.
| Argument | Type | Description |
|---|---|---|
| events* | array | 1–100 events; one invalid event makes AppLovin drop the whole batch, so all are checked first |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"events"
],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"eventSourceUrl",
"userData"
],
"properties": {
"data": {
"type": "object",
"description": "currency, value, transaction_id, tax, shipping, items [{item_id, price, quantity}], search_term",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"name": {
"type": "string",
"description": "page_view, view_item, add_to_cart, begin_checkout, purchase, generate_lead, sign_up, login, search, subscribe, add_payment_info, remove_from_cart, view_cart"
},
"dedupeId": {
"type": "string",
"description": "same id the pixel sent for this event"
},
"userData": {
"type": "object",
"required": [
"ip",
"userAgent"
],
"properties": {
"ip": {
"type": "string",
"description": "visitor's public IP address"
},
"os": {
"enum": [
"ios",
"android",
"desktop_os"
],
"type": "string",
"description": "device operating system: ios, android or desktop_os"
},
"esi": {
"enum": [
"web",
"app"
],
"type": "string",
"description": "event source: web or app"
},
"zip": {
"type": "string",
"description": "visitor postal code"
},
"alart": {
"type": "string",
"description": "AppLovin attribution parameter (alart), as captured"
},
"aleid": {
"type": "string",
"description": "AppLovin id parameter (aleid), as captured"
},
"axwrt": {
"type": "string",
"description": "AppLovin click parameter (axwrt), as captured on the landing page"
},
"email": {
"type": "string",
"description": "visitor email address; hashed before it is sent"
},
"phone": {
"type": "string",
"description": "with country code"
},
"userId": {
"type": "string",
"description": "your own user id for the visitor"
},
"clientId": {
"type": "string",
"description": "client identifier captured by the pixel, if available"
},
"userAgent": {
"type": "string",
"description": "visitor's browser user agent string"
},
"countryCode": {
"type": "string",
"description": "visitor country code, ISO 2-letter"
}
},
"description": "visitor identifiers used for matching; ip and userAgent are required",
"additionalProperties": {}
},
"eventTime": {
"type": "number",
"description": "epoch ms (seconds accepted); default now"
},
"eventSourceUrl": {
"type": "string",
"description": "the full page URL"
}
},
"additionalProperties": {}
},
"description": "1–100 events; one invalid event makes AppLovin drop the whole batch, so all are checked first"
}
}
}Stream conversion events to LinkedIn (Conversions API) — server-side conversion tracking for things that happen where the Insight Tag cannot see them: a CRM deal closing, an offline sale, a phone order, a qualified lead. Needs a rule from create_linkedin_conversion_rule, and that rule needs campaigns associated or nothing is attributed. PASS THE PERSON’S PLAIN EMAIL ADDRESS as `email` — Hermoso applies LinkedIn’s own normalization and SHA-256 hashes it on the server, and the plaintext is never stored or logged. NEVER COMPUTE THE HASH YOURSELF: an invented digest is a well-formed 64-character string that matches nobody, and LinkedIn accepts it with a 201, so the failure is completely silent (`emailSha256` exists only for a source system that already holds real digests). SEND EVERY IDENTIFIER YOU HAVE — email, liFatId (the li_fat_id click id LinkedIn appends to ad click URLs), ipAddress, firstName WITH lastName, an externalId, a lead URN — the match rate is what decides whether the conversion counts at all. Events must have happened in the past 90 DAYS. ACCEPTED IS NOT MATCHED: success means LinkedIn took the events, not that any matched a member, and no API reports the match rate — check attribution in linkedin_ads_report over the following days and never present acceptance as conversions. Up to 5000 events per call. Free.
| Argument | Type | Description |
|---|---|---|
| events* | array | the conversion events — each takes conversionId, conversionHappenedAt (epoch ms or ISO, within 90 days), and at least one identifier: email (PLAIN — hashed here, never hash it yourself), emailSha256, liFatId, ipAddress (IPv4, hashed here), googleAid, acxiomId, firstName WITH lastName, companyName, title, countryCode, lead (urn:li:leadGenFormResponse:…), externalIds (max 1), plus optional amount + currencyCode and an eventId for Insight-Tag deduplication |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
Tell Microsoft Advertising about conversions that happened OFF the website — a phone sale, an in-store purchase, a lead that closed weeks after the click — so smart bidding on that account stops optimising against only what converted online. Each row needs conversionName (an existing OFFLINE conversion goal, matched by name — list_microsoft_ads_conversion_goals shows them), conversionTime (within the last 90 days AND after the click), and an identifier: either microsoftClickId (the msclkid Microsoft appends to the landing-page URL) or, for ENHANCED conversions, a plain email and/or phone. YOU PASS PLAIN VALUES AND THE HASHING HAPPENS SERVER-SIDE: emails are normalized and SHA-256’d to Microsoft’s own published five-step spec, which is verified against Microsoft’s own worked digests. A PHONE MUST ALREADY CARRY ITS COUNTRY CODE (+14255551234) and is refused otherwise — Microsoft’s spec says "normalize to E.164 format with country code" and stops there, and guessing which country a bare 4255551234 belongs to is not a mistake you would ever see: the upload succeeds and matches nobody. SAY THIS WHEN REPORTING: accepted is not the same as visible. Microsoft takes up to 6 hours to show offline conversion data and applies nothing at all in the first 2 hours after a goal is created, and a conversion only counts inside the goal’s own conversion window. Duplicates are silently ignored (the first wins), so re-sending a batch is safe and changes nothing. 0 credits.
| Argument | Type | Description |
|---|---|---|
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| conversions* | array | up to 1000 per request — Microsoft applies each request independently |
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.
| Argument | Type |
|---|
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.
| Argument | Type | Description |
|---|---|---|
| test | boolean | validate only — Pinterest records nothing |
| events* | array | up to 1,000 events |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"events"
],
"properties": {
"test": {
"type": "boolean",
"description": "validate only — Pinterest records nothing"
},
"events": {
"type": "array",
"items": {
"type": "object",
"properties": {
"user": {
"type": "object",
"description": "email, phone, firstName, lastName, gender (f/m/n), dateOfBirth, city, state, zip, countryCode, externalId, maid, ipAddress, userAgent, clickId (the _epik cookie). Plain values are hashed here; a 64-hex digest passes through.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"eventId": {
"type": "string",
"description": "the dedupe key shared with the browser tag — use the order id"
},
"eventName": {
"enum": [
"add_payment_info",
"add_to_cart",
"add_to_wishlist",
"app_install",
"app_open",
"checkout",
"contact",
"custom",
"customize_product",
"find_location",
"initiate_checkout",
"lead",
"page_visit",
"schedule",
"search",
"signup",
"start_trial",
"submit_application",
"subscribe",
"view_category",
"view_content",
"watch_video"
],
"type": "string",
"description": "default page_visit"
},
"eventTime": {
"type": [
"number",
"string"
],
"description": "ISO timestamp or Unix epoch; default now"
},
"customData": {
"type": "object",
"description": "value, currency, orderId, contentIds, contents[{id,itemPrice,quantity}], numItems, contentName, contentCategory, contentBrand, searchString",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"actionSource": {
"enum": [
"web",
"app_android",
"app_ios",
"offline"
],
"type": "string",
"description": "default web"
},
"eventSourceUrl": {
"type": "string",
"description": "page URL where the event happened"
}
}
},
"description": "up to 1,000 events"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| events* | array | up to 1,000 events per call |
| testId | string | a test id from Events Manager > Testing — events sent with it are visible there and NEVER counted in reporting |
| pixelId | string | from list_reddit_ads_pixels — only needed when the account has more than one |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
Report conversions to Snapchat server-side (Conversions API v3) so it can attribute them to ads and optimise delivery toward them — purchases, sign-ups, leads, page views and the rest. pixelId for website and offline events (list_snapchat_ads_pixels), or snapAppId for app events. Pass plain emails and phone numbers: Hermoso normalises and SHA-256 hashes them before they leave. Snapchat's rules, refused for free: each event needs something to match on (email, phone, madid, ipAddress + userAgent, clickId from the ScCid URL parameter, or leadId); WEB events need eventSourceUrl; PURCHASE needs customData.currency and value; nothing older than 7 days; at most 2,000 events per call. validateOnly:true has Snapchat CHECK the events and records nothing — use it first. eventId deduplicates against the browser pixel. Free.
| Argument | Type | Description |
|---|---|---|
| events* | array | conversion events to send, up to 2,000 per call |
| pixelId | string | website / offline events |
| snapAppId | string | app events (Snap App ID) |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| validateOnly | boolean | Snapchat validates and records nothing |
| testEventCode | string | label for Events Manager testing |
SERVER-SIDE CONVERSION TRACKING — report a purchase, signup, form submission or lead to TikTok straight from a server, so TikTok can attribute it and optimise delivery toward it. This is what makes a pixel useful: a pixel with no events is inert, and a conversion campaign optimising toward an event nobody sends has nothing to learn from. ALWAYS OFFER testEventCode FIRST: an event carrying it lands in the Test Events tab of Events Manager and is EXCLUDED from reporting, attribution and optimisation, whereas an event sent WITHOUT it is a real, permanent conversion that TikTok will optimise against and that NO endpoint deletes. The code is copied out of Events Manager in a browser (open the pixel or event set > Test Events > the code button under "Test Server Events") and no API mints one, exactly like a Spark Ads authorization code. eventTime is a Unix timestamp in SECONDS — milliseconds are refused here for free, because TikTok accepts them and reads them as a date tens of thousands of years away, so the event is silently attributed to nothing. Emails, phones and external ids are trimmed, lower-cased, E.164-normalized and SHA-256 hashed by Hermoso exactly as TikTok specifies before anything leaves the process; a value that is already a 64-character hash is passed through untouched, so nothing is ever double-hashed. A phone with no "+" country code is refused rather than guessed. Free.
| Argument | Type | Description |
|---|---|---|
| events* | array | up to 1000 per request — above that TikTok rejects the WHOLE request, not the overflow |
| eventSource* | string | web = a website pixel · app = a mobile app (TikTok gates this behind an allowlist) · offline = a physical store · crm = lead events from a CRM |
| advertiserId | string |
REPORT REAL-WORLD CONVERSIONS — an in-store purchase, a phone booking, a signed contract — so TikTok can attribute them to the ads that caused them and optimise delivery toward them. This is what makes a TikTok campaign measurable for a business whose sale does not happen on a website. Emails and phone numbers are normalized and SHA-256 hashed by Hermoso exactly as TikTok specifies before anything leaves the process, using the SAME implementation as send_tiktok_ads_events; a value that is already a 64-character hash is passed through untouched, and a phone with no "+" country code is refused rather than guessed. EITHER emails OR phone_numbers is REQUIRED on every event — an offline conversion identifying nobody is attributed to nothing. Warning: THE TIMESTAMP IS AN ISO-8601 STRING HERE ("2026-08-19T19:11:01Z"), NOT the Unix-seconds NUMBER that send_tiktok_ads_events takes: TikTok accepts a wrong-shaped one, reads it as some other date and attributes the conversion to nothing, so it is refused here for free. Warning: THERE IS NO TEST MODE: TikTok documents test_event_code on Events API 2.0 and on NEITHER offline endpoint, so everything sent here is a real, permanent conversion that no endpoint deletes. To rehearse the pipeline first, send the same events through send_tiktok_ads_events with eventSource "offline", the same event set id and a testEventCode from Events Manager — those land in the Test Events tab and are excluded from reporting, attribution and optimisation. Warning: REPORTING NEEDS A ROLE, NOT JUST A CONNECTION: TikTok requires the connected user to be an ADMIN or OPERATOR of that advertiser account, measured live — the same token can create and delete Offline Event SETS and still be refused on the events themselves. If that happens Hermoso says so plainly rather than telling anyone to reconnect, because reconnecting cannot grant a role; it has to change in TikTok Business Center. Free.
| Argument | Type | Description |
|---|---|---|
| events* | array | one event goes to TikTok’s single endpoint, several to its bulk one — Hermoso picks |
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.
| Argument | Type | Description |
|---|---|---|
| events* | array | conversion events to report, one entry per purchase, sign-up or lead |
| eventId | string | default Event ID for every event |
| pixelId* | string | the X Pixel (event source) id from Events Manager |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pixelId",
"events"
],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "email address; normalised and SHA-256 hashed on Hermoso's server"
},
"phone": {
"type": "string",
"description": "phone in E.164 form, e.g. +14155550100; hashed before sending"
},
"value": {
"type": "number",
"description": "conversion value in the currency given by currency"
},
"twclid": {
"type": "string",
"description": "X click id from the landing URL's twclid parameter"
},
"eventId": {
"type": "string",
"description": "event id for this one event; overrides the top-level eventId"
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code, e.g. USD"
},
"ipAddress": {
"type": "string",
"description": "visitor IP address; counts only with userAgent or an email or phone"
},
"userAgent": {
"type": "string",
"description": "visitor browser user agent; counts only with ipAddress or an email or phone"
},
"description": {
"type": "string",
"description": "free-text description of the conversion"
},
"numberItems": {
"type": "number",
"description": "number of items in the conversion"
},
"conversionId": {
"type": "string",
"description": "dedup key shared with the X Pixel"
},
"conversionTime": {
"type": "string",
"description": "ISO 8601; default now"
},
"eventSourceUrl": {
"type": "string",
"description": "URL of the page where the conversion happened"
}
}
},
"description": "conversion events to report, one entry per purchase, sign-up or lead"
},
"eventId": {
"type": "string",
"description": "default Event ID for every event"
},
"pixelId": {
"type": "string",
"description": "the X Pixel (event source) id from Events Manager"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| id* | string | REQUIRED. |
| level* | string | REQUIRED. |
| status* | string | REQUIRED. |
| confirm | boolean | REQUIRED to ENABLE — it arms real spend. Pausing needs none. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"id",
"status"
],
"properties": {
"id": {
"type": "string",
"description": "REQUIRED."
},
"level": {
"enum": [
"campaign",
"adgroup",
"keyword",
"negative_keyword",
"ad"
],
"type": "string",
"description": "REQUIRED."
},
"status": {
"enum": [
"ENABLED",
"PAUSED"
],
"type": "string",
"description": "REQUIRED."
},
"confirm": {
"type": "boolean",
"description": "REQUIRED to ENABLE — it arms real spend. Pausing needs none."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| level | string | inferred from the id you pass |
| status* | string | LIVE starts real spend; PAUSED stops delivery |
| confirm | boolean | required true for LIVE |
| campaignId | string | AppLovin campaign id; pass this or creativeSetId |
| creativeSetId | string | AppLovin creative set id; pass this or campaignId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"status"
],
"properties": {
"level": {
"enum": [
"campaign",
"creative_set"
],
"type": "string",
"description": "inferred from the id you pass"
},
"status": {
"enum": [
"LIVE",
"PAUSED"
],
"type": "string",
"description": "LIVE starts real spend; PAUSED stops delivery"
},
"confirm": {
"type": "boolean",
"description": "required true for LIVE"
},
"campaignId": {
"type": "string",
"description": "AppLovin campaign id; pass this or creativeSetId"
},
"creativeSetId": {
"type": "string",
"description": "AppLovin creative set id; pass this or campaignId"
}
}
}Turn automatic credit reloads on or off (admin only): when the balance drops below a threshold, the card on file is charged for a top-up pack — SERVER-SIDE, even with no app open. Requires a saved card, added once in the app at first checkout/top-up; if there's none the tool tells you exactly where to add it. After that one-time card setup, agents can manage auto-reload, top-ups and plan links fully. Members (read-only billing) get an 'ask an admin' message.
| Argument | Type | Description |
|---|---|---|
| enabled* | boolean | true to turn auto-reload on, false to turn it off |
| reloadCredits | integer | how many credits to add each reload — must match a credit pack size (see buy_credits) |
| thresholdCredits | integer | reload when the balance drops below this many credits |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean",
"description": "true to turn auto-reload on, false to turn it off"
},
"reloadCredits": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "how many credits to add each reload — must match a credit pack size (see buy_credits)"
},
"thresholdCredits": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "reload when the balance drops below this many credits"
}
}
}Set WHICH of a connector's accounts this profile is allowed to post to and spend from — Facebook Pages / Instagram / Meta ad accounts, Google Ads customers, LinkedIn company Pages (and the personal profile), Pinterest or Microsoft Advertising ad accounts. Pass ids from list_connector_accounts. This REPLACES the current selection: anything you leave out is un-shared, and an EMPTY list shares nothing (publishing then refuses — it fails closed by design, and the server re-verifies every id against the live connection, so an id the account cannot actually reach is rejected rather than saved). Ask the user which accounts they mean; posting as the wrong Page is a public mistake. Providers: tiktok, x, youtube, threads, bluesky, telegram, pinterest, instagram, meta, google_ads, linkedin, pinterest_ads, linkedin_ads, reddit_ads, apple_ads, microsoft_ads, google_business, google_analytics, snapchat_ads, x_ads, tiktok_ads, google_tag_manager, google_search_console, bing_webmaster. Free.
| Argument | Type | Description |
|---|---|---|
| provider* | string | which connector to scope |
| accountIds* | array | the ids (from list_connector_accounts) this profile may use — an empty array shares nothing |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider",
"accountIds"
],
"properties": {
"provider": {
"enum": [
"tiktok",
"x",
"youtube",
"threads",
"bluesky",
"telegram",
"pinterest",
"instagram",
"meta",
"google_ads",
"linkedin",
"pinterest_ads",
"linkedin_ads",
"reddit_ads",
"apple_ads",
"microsoft_ads",
"google_business",
"google_analytics",
"snapchat_ads",
"x_ads",
"tiktok_ads",
"google_tag_manager",
"google_search_console",
"bing_webmaster"
],
"type": "string",
"description": "which connector to scope"
},
"accountIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the ids (from list_connector_accounts) this profile may use — an empty array shares nothing"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| mode | string | observation (report only, default on Search) or targeting (narrow delivery to these audiences) |
| dryRun | boolean | true = validate with Google and change nothing |
| remove | array | 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 |
| exclude | object | audiences to exclude from the ad group or campaign: {userLists, interests, customSegments} ids |
| adGroupId | string | ad group to target (Search, Display or Demand Gen); one of adGroupId, assetGroupId, campaignId |
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.
| Argument | Type | Description |
|---|---|---|
| dryRun | boolean | true = validate with Google and change nothing |
| confirm | boolean | REQUIRED true to change a LIVE (ENABLED) campaign |
| strategy* | string | the bidding strategy |
| maxCpcUsd | number | MAXIMIZE_CLICKS — the max CPC ceiling; REQUIRED when switching an existing campaign to it |
| campaignId* | string | the campaign to change |
| customerId | string |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | name for a newly created budget |
| confirm | boolean | REQUIRED true to raise the budget of a LIVE campaign |
| customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| dailyBudgetUsd* | number | daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) |
| loginCustomerId | string | manager id if operating through an MCC |
| budgetResourceName | string | existing budget to UPDATE — omit to CREATE a new budget |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"dailyBudgetUsd"
],
"properties": {
"name": {
"type": "string",
"description": "name for a newly created budget"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to raise the budget of a LIVE campaign"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"dailyBudgetUsd": {
"type": "number",
"description": "daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000)"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"budgetResourceName": {
"type": "string",
"description": "existing budget to UPDATE — omit to CREATE a new budget"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| goals | array | conversion goals to set, as a list of {category, origin, biddable}; omit to read the current goals |
| dryRun | boolean | validate with Google, change nothing |
| confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
| campaignId | string | omit for the account goals |
| customerId | string | omit to use the selected default account |
| loginCustomerId | string | manager id if operating through an MCC |
| useAccountGoals |
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.
| Argument | Type | Description |
|---|---|---|
| dryRun | boolean | validate with Google, write nothing |
| confirm | boolean | REQUIRED true to change a LIVE (ENABLED) campaign |
| display | boolean | serve on the Google Display Network (target_content_network) |
| campaignId* | string | the campaign to change |
| customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| googleSearch | boolean | serve on Google Search (target_google_search) |
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.
| Argument | Type | Description |
|---|---|---|
| dryRun | boolean | validate with Google, change nothing |
| confirm | boolean | true only after the user approved a change to a LIVE campaign or a delete |
| devices | object | campaign-level device multipliers |
| schedule | array | ad schedule slots; replaces the current schedule unless scheduleMode is add |
| adGroupId | string | for adGroupDevices |
| locations | array | bid adjustments for locations the campaign already targets |
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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id (level:"ad" — pass adGroupId too) |
| level | string | what to change — default campaign |
| status* | string | ENABLED = start spending; PAUSED = stop; REMOVED = permanent |
| confirm | boolean | REQUIRED true to ENABLE (real spend) or to REMOVE (permanent) |
| adGroupId | string | ad group id (level:"adGroup", or with adId for level:"ad") |
| campaignId | string | campaign id (level:"campaign") |
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.
| Argument | Type | Description |
|---|---|---|
| dryRun | boolean | true = validate with Google and change nothing |
| confirm | boolean | REQUIRED true to change a LIVE (ENABLED) campaign |
| languages | array | ISO language codes, e.g. ["en","es"] |
| locations | array | location NAMES to target |
| campaignId* | string | the campaign to target |
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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad to set tracking on; use with level ad |
| level | string | follows the id you pass |
| dryRun | boolean | true = validate with Google and change nothing |
| confirm | boolean | needed to change the tracking redirect on a LIVE campaign |
| adGroupId | string | ad group to set tracking on; use with level adGroup |
| campaignId | string | campaign to set tracking on; use with level campaign |
| customerId | string | 10-digit account id; omit to use the brand's selected default account |
| utmDefault | boolean |
Switch the AUTOMATIC HubSpot lead sync on or off for this brand: enabled, everyMinutes (a whole number from 15 to 1440; default 60 — anything outside that is refused, not clamped), optional platforms (meta, linkedin, tiktok, x, snapchat, chatgpt, reddit; default every connected one). Each run is sync_leads_to_hubspot reading from the last successful run, so a new lead reaches the CRM within minutes and nothing is ever created twice. If HubSpot is disconnected a run records that and the schedule backs off to once a day — no emails. runNow:true runs the first one straight away. Answers with the stored setting and the armed run, read back. Free. Needs HubSpot connected to switch on.
| Argument | Type | Description |
|---|---|---|
| runNow | boolean | true = run the first sync immediately |
| enabled* | boolean | true turns the automatic lead sync on; false turns it off |
| platforms | array | omit for every connected one |
| everyMinutes | number | 15–1440, default 60 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"enabled"
],
"properties": {
"runNow": {
"type": "boolean",
"description": "true = run the first sync immediately"
},
"enabled": {
"type": "boolean",
"description": "true turns the automatic lead sync on; false turns it off"
},
"platforms": {
"type": "array",
"items": {
"enum": [
"meta",
"linkedin",
"tiktok",
"x",
"snapchat",
"chatgpt",
"reddit"
],
"type": "string"
},
"description": "omit for every connected one"
},
"everyMinutes": {
"type": "number",
"description": "15–1440, default 60"
}
}
}Change a LinkedIn campaign’s daily and/or total budget. On a LIVE (ACTIVE) campaign this changes real spend immediately — and LinkedIn can spend up to 150% of a daily budget on a high-opportunity day — so show the user the new amount, get an explicit yes, then call with confirm:true. The currency must match the ad account’s. Read back after the change.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true to change the budget of a LIVE campaign |
| campaignId* | string | campaign id from list_linkedin_ads_campaigns |
| adAccountId | string | LinkedIn ad account id from list_linkedin_ads_campaigns |
| dailyBudget | number | new daily budget in the ad account currency |
| totalBudget | number | new total budget in the ad account currency |
| currencyCode | string | must match the ad account currency, e.g. USD |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change the budget of a LIVE campaign"
},
"campaignId": {
"type": "string",
"description": "campaign id from list_linkedin_ads_campaigns"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id from list_linkedin_ads_campaigns"
},
"dailyBudget": {
"type": "number",
"description": "new daily budget in the ad account currency"
},
"totalBudget": {
"type": "number",
"description": "new total budget in the ad account currency"
},
"currencyCode": {
"type": "string",
"description": "must match the ad account currency, e.g. USD"
}
}
}Turn a LinkedIn campaign group, campaign or CREATIVE (the ad itself) on (ACTIVE) or off (PAUSED). Pass level:"campaign" + campaignId, level:"campaignGroup" + campaignGroupId, or level:"creative" + creativeId. All three tiers must be ACTIVE for an ad to serve — activating only the campaign leaves a DRAFT creative sitting there showing nothing. ACTIVATING STARTS REAL AD SPEND — you MUST first show the user the campaign and its budget, get an explicit yes, then call with status:"ACTIVE" and confirm:true. Pausing is always safe. The resulting status is READ BACK from LinkedIn along with its servingStatuses before you are told it took: LinkedIn defers validation on drafts, so activation is exactly where a hidden problem surfaces, and the note reports what LinkedIn actually says rather than what was requested.
| Argument | Type | Description |
|---|---|---|
| level | string | default campaign |
| status* | string | ACTIVE starts spend (needs confirm); PAUSED stops it; ARCHIVED is permanent |
| confirm | boolean | REQUIRED true to set ACTIVE (real spend) |
| campaignId | string | campaign id when level is campaign |
| creativeId | string | creative (ad) id when level is creative |
| adAccountId | string | LinkedIn ad account id from list_linkedin_ads_campaigns |
Dynamic UTM tracking for LinkedIn ads: parameters LinkedIn appends to every ad’s landing page on a campaign (or, level:"account", across the whole ad account), filled in per click. standard:true sets utm_source=linkedin, utm_medium=paid_social, utm_campaign={campaign name}, utm_content={creative id}. Or pass dynamic {key: ACCOUNT_ID | ACCOUNT_NAME | CAMPAIGN_GROUP_ID | CAMPAIGN_GROUP_NAME | CAMPAIGN_ID | CAMPAIGN_NAME | CREATIVE_ID | CREATIVE_NAME} and/or custom {key: fixed value}. A new set REPLACES the previous one; clear:true removes it; call with no parameters to read what is set. Ads do not go back through review. Read back from LinkedIn. Free.
| Argument | Type | Description |
|---|---|---|
| clear | boolean | true removes all dynamic tracking parameters |
| level | string | default campaign when campaignId is given |
| custom | object | UTM key mapped to a fixed value, e.g. {utm_source: newsletter}; a new set replaces the old |
| dynamic | object | UTM key mapped to a LinkedIn macro, e.g. {utm_campaign: CAMPAIGN_NAME}; a new set replaces the old |
| standard | boolean | true sets the standard UTM set: source linkedin, medium paid_social, campaign and content filled per click |
| campaignId | string | campaign to set tracking on; omit for account-level tracking with level account |
SET the price, availability or stock level of one Shopping product at ONE physical store (kind:"local", keyed by storeCode) or in ONE region (kind:"regional", keyed by region). This is what stops a Shopping ad advertising something the nearest store has sold out of. Google's insert REPLACES the whole entry, so this tool reads what is already stored, merges your fields over it, and reports which stored fields it preserved; pass an explicit null to CLEAR one. The API publishes no enum for availability, so it accepts anything and only Google's local inventory data specification says which values actually serve: In stock, Limited availability, On display to order, Out of stock, submitted in English. An unrecognised value is warned about here rather than refused, because refusing would mean inventing an enum Google does not publish. Google states it can take up to 30 minutes before the product itself reflects the change, so a product read straight afterwards can still show the old value and be correct. Free.
| Argument | Type | Description |
|---|---|---|
| kind* | string | local = one physical store (storeCode), regional = one Google region (region) |
| price | string | null | "12.99 USD". A bare number is refused because a price with no currency is not a price. null clears it |
| region | string | required for kind:'regional' — the Merchant Center region id. Immutable |
| offerId* | string | the product's own id in the feed |
| quantity |
Turn a campaign ON (ACTIVE) or OFF (PAUSED). ACTIVATING STARTS REAL AD SPEND — you MUST first show the user the campaign name + its daily budget, get an explicit yes, then call with status:"ACTIVE" and confirm:true. Pausing is always safe. Needs ads-management permission.
| Argument | Type | Description |
|---|---|---|
| status* | string | ACTIVE = start spending; PAUSED = stop |
| confirm | boolean | REQUIRED true to activate (real spend) — set only after the user explicitly approved the budget |
| campaignId* | string | the campaign id (from create_meta_campaign) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"status"
],
"properties": {
"status": {
"enum": [
"ACTIVE",
"PAUSED"
],
"type": "string",
"description": "ACTIVE = start spending; PAUSED = stop"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to activate (real spend) — set only after the user explicitly approved the budget"
},
"campaignId": {
"type": "string",
"description": "the campaign id (from create_meta_campaign)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | the user said yes. Required for ProfileExpansionEnabled=true, AllowImageAutoRetrieve=true and any AIDetectedContentWatermarkPolicy value |
| accountId | string | older name for adAccountId (MCP only) |
| properties* | 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>"} |
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.
| Argument | Type | Description |
|---|---|---|
| action | string | default add |
| confirm | boolean | true required on a LIVE campaign when targeting or removing |
| exclude | boolean | true = exclude this audience instead of targeting it |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId | string | ad group to target or exclude the audience on; pass this or campaignId |
| audienceId | string | list_microsoft_ads_audiences |
| campaignId | string | campaign to target or exclude the audience on; pass this or adGroupId |
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.
| Argument | Type | Description |
|---|---|---|
| optIns* | 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 |
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.
| Argument | Type | Description |
|---|---|---|
| maxCpc | number | bid cap in account currency, for MaxClicks and MaxConversions |
| confirm | boolean | REQUIRED true on a LIVE campaign |
| accountId | string | older name for adAccountId (MCP only) |
| targetCpa | number | target cost per conversion in account currency, for MaxConversions |
| campaignId* | string | Microsoft campaign id whose bid strategy changes |
| targetRoas | number | ratio, e.g. 4 = 400% |
| adAccountId |
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true to change the budget of a LIVE campaign |
| accountId | string | older name for adAccountId (MCP only) |
| campaignId* | string | the campaign whose budget changes |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| dailyBudget* | number | new daily budget in the account’s currency |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"dailyBudget"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change the budget of a LIVE campaign"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "the campaign whose budget changes"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"dailyBudget": {
"type": "number",
"description": "new daily budget in the account’s currency"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| add | array | profile criteria to add: [{profileType, profileId, bidAdjustment}] |
| update | array | bid adjustments to change on existing criteria: [{criterionId, bidAdjustment}] |
| confirm | boolean | REQUIRED true when the campaign is ACTIVE |
| accountId | string | older name for adAccountId (MCP only) |
| campaignId* | string | Microsoft campaign id to change LinkedIn profile targeting on |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id (level:"ad" — pass adGroupId too) |
| level | string | what to change — default campaign |
| status* | string | Active = start spending; Paused = stop |
| confirm | boolean | REQUIRED true to set Active (real spend) |
| accountId | string | older name for adAccountId (MCP only) |
| adGroupId | string | ad group id (level:"adGroup", or with adId for level:"ad") |
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true when the campaign is live |
| campaignId* | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true when the campaign is live"
},
"campaignId": {
"type": "string",
"description": "campaign id whose budget changes"
},
"dailyBudget": {
"type": "number",
"description": "daily cap in the account currency; ChatGPT Ads minimum is 25.00"
},
"lifetimeBudget": {
"type": "number",
"description": "lifetime cap in the account currency; no 25.00 minimum applies"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| amount* | number | daily limit in the account currency; 0 stops all account spend |
| confirm | boolean | true applies the change; needs expectedRevision as well |
| endDate | string | YYYY-MM-DD, exclusive |
| newRule |
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.
| Argument | Type | Description |
|---|---|---|
| action | string | create (default) sets up or rotates credentials; activate or pause switches delivery only |
| feedId* | string | product feed id |
| sshPublicKey | string | SSH public key, used with authenticationMethod ssh_key |
| authenticationMethod | string | password (returned once in the reply) or ssh_key |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"feedId"
],
"properties": {
"action": {
"enum": [
"create",
"activate",
"pause"
],
"type": "string",
"description": "create (default) sets up or rotates credentials; activate or pause switches delivery only"
},
"feedId": {
"type": "string",
"description": "product feed id"
},
"sshPublicKey": {
"type": "string",
"description": "SSH public key, used with authenticationMethod ssh_key"
},
"authenticationMethod": {
"enum": [
"password",
"ssh_key"
],
"type": "string",
"description": "password (returned once in the reply) or ssh_key"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| signingSecret | string | your own webhook signing secret; only used with destinationUrl, returned once |
| destinationUrl | string | omit to deliver the leads to Hermoso |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"signingSecret": {
"type": "string",
"description": "your own webhook signing secret; only used with destinationUrl, returned once"
},
"destinationUrl": {
"type": "string",
"description": "omit to deliver the leads to Hermoso"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| keywords* | array | negative keywords; currently unavailable because OpenAI no longer serves this endpoint |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"keywords"
],
"properties": {
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "negative keywords; currently unavailable because OpenAI no longer serves this endpoint"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| ioId | string | insertion-order id, when billing uses one |
| name | string | window name |
| amount | number | spend ceiling for the whole account over the window, in the account currency |
| endDate | string | YYYY-MM-DD, end of the window (exclusive; the window ends the day before) |
| windowId | string | spend-limit window id to edit; omit to create a new window |
| startDate |
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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id to change; use with level ad |
| level | string | inferred from which id you pass — default campaign. "account" is the whole ad account and takes no id. |
| status* | string | active = start spending; paused = stop; archived = permanent (not available at level "account") |
| confirm | boolean |
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.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true to change the budget of a LIVE campaign |
| campaignId* | string | the campaign whose budget changes |
| adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change the budget of a LIVE campaign"
},
"campaignId": {
"type": "string",
"description": "the campaign whose budget changes"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id, from list_pinterest_ads_campaigns"
},
"dailyBudget": {
"type": "number",
"description": "daily cap in the ad account currency; use one budget type per campaign"
},
"lifetimeBudget": {
"type": "number",
"description": "lifetime cap in the ad account currency; use instead of dailyBudget"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id to change; use with level ad |
| level | string | what to change — default campaign |
| status* | string | ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire (Pinterest’s delete) |
| confirm | boolean | REQUIRED true for ACTIVE (real spend) or ARCHIVED (irreversible retirement) |
| adGroupId | string | ad group id to change; use with level adGroup |
| campaignId | string | campaign id to change; use with level campaign |
| adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
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.
| Argument | Type | Description |
|---|---|---|
| id* | string | id of the campaign, ad group or ad |
| kind* | string | campaign, ad_group or ad: which type the id refers to |
| status* | string | 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 |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
Change a workspace member’s role — admin (full access incl. billing) or member (read-only on billing). A privilege change: confirm the exact person + new role with the user, then call with confirm:true.
| Argument | Type | Description |
|---|---|---|
| role* | string | the new role |
| email* | string | the member’s email |
| confirm | boolean | REQUIRED true |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email",
"role"
],
"properties": {
"role": {
"enum": [
"admin",
"member"
],
"type": "string",
"description": "the new role"
},
"email": {
"type": "string",
"description": "the member’s email"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
}
}
}Change the budget on a Snapchat AD SQUAD. BUDGETS LIVE ON THE AD SQUAD, not on the campaign — a campaign-level ask is refused by name rather than silently patching nothing. NEEDS confirm:true, and without it NOTHING CHANGES: you get a sentence naming the ad squad AS READ FROM SNAPCHAT and the amount, which you must show the user first. State a plain amount in dailyBudget and Hermoso converts to micro-currency; never pass both units. The new budget is READ BACK — report what Snapchat stored, in both micro and real money, not what you sent.
| Argument | Type | Description |
|---|---|---|
| id* | string | the ad squad id |
| level | string | adsquad — the only tier that holds a budget on Snapchat |
| confirm | boolean | REQUIRED true — without it nothing changes and you get the sentence to show the user |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| dailyBudget | number | in the ad account’s own currency — minimum 5 |
| lifetimeBudget | number | lifetime budget in the ad account's currency; an alternative to dailyBudget |
| dailyBudgetMicro | number |
THE ONE SWITCH THAT ARMS REAL MONEY ON SNAPCHAT. ACTIVE starts real spend on the next auction; PAUSED stops it. EVERY change needs confirm:true, and WITHOUT confirm NOTHING CHANGES — you get a sentence naming each object AS READ FROM SNAPCHAT, with its real name and its current status, which you must show the user before asking for a yes. Confirming proves the caller meant to change SOMETHING; only reading the object back proves they aimed at the right one. EVERY TIER must be ACTIVE for a single impression to serve — a live ad under a paused ad squad shows nothing. THE ANSWER IS THE READ-BACK: report what Snapchat STORED per id, never the status you asked for. Snapchat has NO delete status — removal is a real DELETE verb, so use delete_snapchat_ads_object.
| Argument | Type | Description |
|---|---|---|
| ids* | array | the objects to change |
| level* | string | which tier these ids belong to |
| status* | string | ACTIVE arms real spend |
| confirm | boolean | REQUIRED true — without it nothing changes and you get the sentence to show the user |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"ids",
"status"
],
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "the objects to change"
},
"level": {
"enum": [
"campaign",
"adsquad",
"ad"
],
"type": "string",
"description": "which tier these ids belong to"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED"
],
"type": "string",
"description": "ACTIVE arms real spend"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — without it nothing changes and you get the sentence to show the user"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}Set what content this TikTok ad account’s ads may appear next to. THREE RULES TIKTOK IMPOSES AND ONE THING THAT CANNOT BE UNDONE. (1) The suitability controls move as a TRIO: TikTok requires brandSafetyType, categoryExclusionIds and verticalSensitivityId "simultaneously", so read the current values with get_tiktok_ads_brand_safety and send all three — a partial update is refused here rather than silently doing something else. (2) NO_BRAND_SAFETY is a tier TikTok REPORTS but does not ACCEPT: the write enum is EXPANDED_INVENTORY / STANDARD_INVENTORY / LIMITED_INVENTORY only, so an account can be moved off it through the API and not back onto it. (3) Category exclusions and vertical sensitivity are valid only under STANDARD or LIMITED. Warning: coverAllObjectives IS A ONE-WAY DOOR — TikTok: "Once set to true, this setting cannot be updated back to false." Turning it on widens the settings from Reach / Video Views / Community Interaction to every objective, permanently, so that direction is confirm-gated and nothing else here is. AND MIND THE SCOPE: TikTok applies this to future Smart+ campaigns and explicitly NOT to regular campaigns created through /campaign/create/, which is what create_tiktok_ads_campaign uses. The reply is TikTok’s own stored row read back, so it reports what actually changed rather than what was sent. EXPANDED_INVENTORY is allowlist-only per advertiser; if TikTok refuses it the remedy is your TikTok rep, not a different field.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true only to turn coverAllObjectives ON for the first time, because that is irreversible |
| advertiserId | string | from list_tiktok_ads_accounts |
| brandSafetyType | string | required whenever you change any suitability control |
Change the budget on a TikTok campaign (level:"campaign") or ad group (level:"adgroup"), in the advertiser’s own currency. It needs confirm:true, and CALLING IT WITHOUT confirm CHANGES NOTHING and returns the sentence naming the object and the new amount — show the user that first. Everything Hermoso creates on TikTok is forced paused, so a budget change on one of those spends nothing; on an object somebody has ENABLED it takes effect on the next auction, and LOWERING the budget is the fastest way to slow real money down short of set_tiktok_ads_status(status:"DISABLE"). The new budget is READ BACK from TikTok — report what it returns, not what you sent, and say so plainly when the read-back could not confirm it.
| Argument | Type | Description |
|---|---|---|
| id* | string | the campaign or ad group id |
| level | string | which tier holds the budget — default campaign |
| budget* | number | the new budget in the advertiser’s own currency — must be above zero |
| confirm | boolean | REQUIRED true — call without it first to see exactly what would change |
| budgetMode | string | default BUDGET_MODE_DAY |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
TURN_ON IS THE SWITCH THAT HANDS A RULE TO TIKTOK TO RUN UNATTENDED, and it is gated in two classes because the two are not the same act. A rule whose only actions are TURN_OFF, MESSAGE or a DECREASE cannot start or raise spend — the worst it does is under-deliver, which is recoverable by turning it off — so it needs confirm:true and nothing else. A rule that can TURN_ON an object or INCREASE / ADJUST_TO a budget or bid is standing permission to spend real money with nobody present, so it needs confirm:true AND confirmScope set to the scopeToken that list_tiktok_ads_rules prints. That token is computed from the rule AS TIKTOK STORES IT: confirm:true proves the caller meant to arm SOMETHING, and only the echo proves they aimed at the rule they actually inspected rather than one a teammate has edited since. CALLING WITHOUT THE GATE CHANGES NOTHING and returns the sentence describing exactly what the rule will be able to do — show the user that, get an unambiguous yes, then confirm. TURN_OFF and DELETE only ever reduce what runs unattended, so both take confirm:true and no echo; prefer TURN_OFF, because TikTok publishes no way to restore a deleted rule and turning one off is reversible. THE ANSWER IS THE READ-BACK: the result carries the status TikTok stored per rule, and says so when it could not confirm. Free.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED true for every operation — call without it first to see what would happen |
| ruleIds* | array | the rules to change |
| operateType* | string | TURN_ON = start running it unattended; TURN_OFF = stop, reversible; DELETE = remove, NOT reversible |
THE ONE SWITCH THAT ARMS REAL MONEY ON TIKTOK. Pass level ("campaign", "adgroup" or "ad"), the ids, and a status: ENABLE starts real spend on the next auction, DISABLE stops it, DELETE removes the objects (TikTok models removal as a STATUS — it publishes no delete verb — which is why delete_tiktok_ads_object is a thin wrapper over this same route). EVERY status change here needs confirm:true, and CALLING IT WITHOUT confirm CHANGES NOTHING and hands back the sentence describing exactly what would happen — show the user that, get an unambiguous yes, then confirm. EVERY TIER HAS TO BE ENABLED FOR AN IMPRESSION TO SERVE: Hermoso creates all three paused, so enabling the campaign alone does nothing while its ad group and ad are still disabled, and each level is a separate call. Ids can be passed in bulk, but TikTok’s QPS is 1 and calls are serialized, so a long list is simply slow. THE ANSWER IS THE READ-BACK: the result carries what TikTok STORED per id, plus a note when the read-back did not return every id — repeat that rather than the status you asked for.
| Argument | Type | Description |
|---|---|---|
| ids* | array | the campaign / ad group / ad ids to change |
| level* | string | which tier the ids belong to |
| status* | string | ENABLE = start real spend; DISABLE = stop; DELETE = remove (TikTok has no delete verb, removal is a status) |
| confirm | boolean | REQUIRED true — call without it first to see exactly what would change |
Turn TikTok's 'Ad authorization' setting ON or OFF for a post the authorized account owns — THIS IS WHERE A SPARK ADS AUTHORIZATION CODE COMES FROM. For somebody ELSE's post the code still comes from that creator, copied out of the TikTok app. Turning it on mints the code, which you then hand to authorize_tiktok_ads_spark_post so an ad account may promote the post. IT IS CONFIRM-GATED ON THE WAY ON, and not because it spends: TikTok changes the post's privacy to 'Available for Ads', sends it to their ad review team, says the post 'may also appear as an ad on third party platforms', and treats the call as accepting their Advertising Content Terms on the owner's behalf. Turning it OFF needs no confirm — but TikTok REFUSES to turn it off while an active Spark Ad is using the post, so pause those campaigns first. AuthorizationDays must be one of 7, 30, 60, 180, 365. THE ANSWER IS THE READ-BACK: the reply carries the post's real authorization status and its code, because TikTok's own response body is empty. NEEDS THE TIKTOK ACCOUNT AUTHORIZATION.
| Argument | Type | Description |
|---|---|---|
| itemId* | string | the TikTok post id |
| confirm | boolean | REQUIRED true when enabling — it makes the post publicly promotable and accepts TikTok’s advertising terms on the owner’s behalf |
| enabled* | boolean | true turns Ad authorization ON (and mints the Spark Ads code); false turns it off |
| authorizationDays | number | 7 | 30 | 60 | 180 | 365 — TikTok publishes exactly those five; default 30 |
Enable, pause or delete Smart+ campaigns, ad groups or ads (up to 20 ids per call). ENABLE starts REAL SPEND on the next auction and DELETE is permanent (a deleted campaign takes its ad groups and ads with it), so both require confirm:true after the user says yes. DISABLE (pause) is never gated. The reply is read back from TikTok and names any id whose new state it could not confirm.
| Argument | Type | Description |
|---|---|---|
| ids* | any | one or more Smart+ ids at that level, up to 20; from list_tiktok_smart_campaigns |
| level* | string | which Smart+ tier the ids belong to: campaign, adgroup or ad |
| status* | string | ENABLE starts spend (needs confirm); DISABLE pauses; DELETE is permanent (needs confirm) |
| confirm | boolean | required for ENABLE (real money) and DELETE (permanent) |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"ids",
"status"
],
"properties": {
"ids": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "one or more Smart+ ids at that level, up to 20; from list_tiktok_smart_campaigns"
},
"level": {
"enum": [
"campaign",
"adgroup",
"ad"
],
"type": "string",
"description": "which Smart+ tier the ids belong to: campaign, adgroup or ad"
},
"status": {
"enum": [
"ENABLE",
"DISABLE",
"DELETE"
],
"type": "string",
"description": "ENABLE starts spend (needs confirm); DISABLE pauses; DELETE is permanent (needs confirm)"
},
"confirm": {
"type": "boolean",
"description": "required for ENABLE (real money) and DELETE (permanent)"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Pause or ACTIVATE an X ads CAMPAIGN (campaignId) or ONE LINE ITEM inside it (lineItemId) — pass exactly one. ACTIVATING STARTS REAL SPEND on the next auction, so it requires confirm:true — this is the only switch on X that arms money. Tell the user the budget and what will start spending BEFORE you pass confirm. DELIVERY ON X IS THE AND OF BOTH LEVELS: an ACTIVE line item under a PAUSED campaign serves nothing, so the result reads the PARENT back too and states whether anything can actually spend rather than letting you infer it. Pausing a line item is the REVERSIBLE way to take one ad group out of delivery — deleting it is not.
| Argument | Type | Description |
|---|---|---|
| status* | string | ACTIVE starts real spend; PAUSED stops delivery |
| confirm | boolean | required to set ACTIVE — real money |
| accountId | string | older name for adAccountId (MCP only) |
| campaignId | string | the whole campaign — pass this OR lineItemId |
| lineItemId | string | one ad group — pass this OR campaignId |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
Create a SHARING LINK to a OneDrive file or folder (fileId from list_onedrive_files): type view (read-only, default), edit, or embed (an <iframe> link, personal OneDrive only); scope anonymous (anyone with the link, default), organization (work or school OneDrive only) or users (only people you invite); optional expiresAt (date or ISO) and password (on a personal OneDrive both need a Microsoft 365 subscription; the refusal says so). An ANONYMOUS EDIT link lets anyone holding it change or delete the file, so it needs confirm:true after you show the user that; every other link is created straight away. If Hermoso already made the same kind of link, OneDrive returns that one and the reply says it was reused. Read back from the item's permission list. 0 credits. Needs OneDrive connected.
| Argument | Type | Description |
|---|---|---|
| type | string | view (default), edit, or embed (personal OneDrive only) |
| scope | string | anonymous (default, anyone with the link), organization (work or school), or users (invited people only) |
| fileId* | string | OneDrive file or folder id from list_onedrive_files |
| confirm | boolean | required for an anonymous edit link |
| password | string | personal OneDrive with Microsoft 365 |
| expiresAt | string | e.g. 2026-11-30 or 2026-11-30T17:00:00Z |
Read Snapchat ad performance — impressions, spend, swipes and video quartiles — at ad account, campaign, ad squad or ad level. THREE THINGS TO KNOW BEFORE CALLING: granularity is required (TOTAL is the default); DAY and HOUR granularity REQUIRE startTime and endTime AND both must land exactly on the start of an hour, which Snapchat refuses otherwise and Hermoso refuses for free before spending the call; and SPEND COMES BACK IN MICRO-CURRENCY, so divide by 1,000,000 before quoting money to anyone. It changes nothing on the ad account; with async:true it starts a report file on Snapchat that is kept for seven days. Free.
| Argument | Type | Description |
|---|---|---|
| id | string | the object to report on — defaults to the ad account itself for level:adaccount |
| async | boolean | for long ranges or wide breakdowns: starts a report run on Snapchat that builds a downloadable CSV/Excel file (kept seven days) and returns its reportRunId |
| level | string | default campaign |
| fields | string | comma-separated metrics — default impressions,spend,swipes |
| endTime | string | ISO 8601, on the start of an hour |
| breakdown | string | object-level breakdown: ad, adsquad (campaign stats only) or campaign (ad-account stats only) |
| omitEmpty | boolean | drop days/entities with zero on every metric |
Read one of this workspace’s data stores by key, for visibility into what the app holds — playbooks, swipefile, saved locations, avatars, creations, chats, brand, memory, skills. Read-only, free. Allowed keys: heist.memory.v1, heist.skills.v1, heist.playbooks.v1, heist.avatars.v1, heist.locations.v1, heist.chats.v1, heist.creations.v1, heist.assets.v1, heist.brand.v1, adInspo.swipefile.v1. (The typed tools — list_memory / list_skills / get_brand — are friendlier for those; use store_get for the rest.)
| Argument | Type | Description |
|---|---|---|
| key* | string | the store key to read (one of the allowlisted keys) |
| limit | number | max array items to return (default 50) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "the store key to read (one of the allowlisted keys)"
},
"limit": {
"type": "number",
"description": "max array items to return (default 50)"
}
}
}The brand's Stripe balance (available and pending, per currency) and the most recent payouts with status and arrival date. Read-only, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| limit | number | number of recent payouts to show, 1 to 100; default 25 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "number of recent payouts to show, 1 to 100; default 25"
}
}
}Revenue from the brand's OWN Stripe account: gross, refunds, net and succeeded-charge count per day/week/month, new customers in the window, and active subscriptions + MRR where the key can read them. This is the money side of the loop — read an ad, a hook or a launch against real revenue instead of clicks. Default window is the last 30 days; a window with no charges answers a real zero, not a failed read. Read-only, never moves money, free. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector: paste a restricted key from Developers > Restricted keys).
| Argument | Type | Description |
|---|---|---|
| since | string | YYYY-MM-DD (default 30 days ago) |
| until | string | YYYY-MM-DD (default today) |
| currency | string | ISO code to isolate one currency, e.g. USD; omit to sum every currency as-is |
| granularity | string | default day |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"since": {
"type": "string",
"description": "YYYY-MM-DD (default 30 days ago)"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD (default today)"
},
"currency": {
"type": "string",
"description": "ISO code to isolate one currency, e.g. USD; omit to sum every currency as-is"
},
"granularity": {
"enum": [
"day",
"week",
"month"
],
"type": "string",
"description": "default day"
}
}
}Give Bing a page's content DIRECTLY instead of waiting for it to crawl — for a page Bingbot renders badly or cannot reach. YOU PASS THE HTML AND NOTHING ELSE: Microsoft's httpMessage parameter is a base64 raw HTTP response whose status line and every header must end CRLF with exactly two CRLFs before the body, and an LF-only message is accepted with a 200 while indexing nothing useful — a silent wrong answer — so Hermoso builds that framing and computes Content-Length itself. Optional `structuredData` carries JSON-LD for non-HTML content such as images or PDFs; `dynamicServing` is left at "none" unless the site really does serve different content per device, because declaring otherwise is a claim about the customer's infrastructure. This spends the CONTENT submission budget, which is separate from the URL one and is read first — check both with bing_webmaster_submission_quota. Max 10MB per submission. 0 credits.
| Argument | Type | Description |
|---|---|---|
| url* | string | the full URL on this site that this content belongs to |
| html* | string | the page content Bing should index for that URL |
| status | number | HTTP status for the message, default 200 |
| headers | object | extra response headers. Content-Length is always computed and never taken from you. |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
| contentType |
Register a sitemap with Bing so it discovers new and changed pages by itself — the durable twin of submit_bing_webmaster_urls, which spends a small per-site daily quota every time. Takes the sitemap's FULL URL, not a path. Bing accepts Sitemap, RSS 2.0, Atom 0.3, Atom 1.0 and plain text files (Microsoft's own list). A sitemap hosted on ANOTHER host is not refused — Bing allows that when the other host is also verified — but it is called out, because it is the likeliest reason one silently never crawls. SubmitFeed returns no body whatsoever, so the answer is read back from Bing's own feed list and reports what Bing says rather than that the call returned. 0 credits.
| Argument | Type | Description |
|---|---|---|
| feedUrl* | string | the FULL sitemap URL, e.g. "https://example.com/sitemap.xml" |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"feedUrl"
],
"properties": {
"feedUrl": {
"type": "string",
"description": "the FULL sitemap URL, e.g. \"https://example.com/sitemap.xml\""
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}Submit URLs to Bing for indexing — the fastest way to get a new or changed page crawled by Bing, and therefore into Copilot and ChatGPT web search. THE SITE'S REMAINING DAILY QUOTA IS READ FIRST and a batch that would exceed it is refused with the real number rather than half-submitted, because Bing rejects an over-quota batch as a whole and a partial failure leaves you unable to say which URLs made it. Bing accepts at most 500 per batch (Microsoft's own limit) and the site's daily quota is usually far lower. THE CONFIRMATION IS BING'S QUOTA COUNTER MOVING: its submit methods return no body at all, so the answer reports the counter before and after rather than claiming success from a 200. 0 credits.
| Argument | Type | Description |
|---|---|---|
| urls* | array | full URLs on THIS site — a URL on another host is refused before anything is submitted |
| siteUrl* | string | the exact site string from list_bing_webmaster_sites |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"urls"
],
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "full URLs on THIS site — a URL on another host is refused before anything is submitted"
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
}
}
}Ping the IndexNow search engines that pages were added, updated or deleted — Bing, Yandex, Naver, Seznam.cz and Yep, all reached by ONE submission. Important: GOOGLE DOES NOT PARTICIPATE IN INDEXNOW and never has, so say that plainly rather than letting anyone expect a Google effect; for Google use submit_search_console_sitemap and search_console_url_inspection. NO CONNECTION IS NEEDED, because the credential belongs to the USER: they generate a key, host it as a plain text file on their own web server, and any engine fetching that file is the whole ownership check. HERMOSO VERIFIES THAT FILE BEFORE SUBMITTING — that it is reachable and that its contents are exactly the key — and refuses with an explanation if not, because we have no write access to a customer's web root and cannot place it for them. If the key file is moved off the site root with `keyLocation`, it authorises ONLY URLs under that directory, and URLs outside it are refused before anything is sent (IndexNow rejects the whole batch for one out-of-scope URL). Up to 10,000 URLs per submission, all on the one host. HTTP 202 is a SUCCESS ("received, validation pending"), not a reason to retry. 0 credits.
| Argument | Type | Description |
|---|---|---|
| key* | string | the IndexNow key — 8-128 letters, numbers and dashes — ALREADY hosted as <key>.txt on that site |
| host* | string | the bare hostname that owns these URLs, e.g. "example.com" |
| urls* | array | the full URLs that were added, updated or deleted, all on `host` |
| keyLocation | string | the full URL of the key file if it is NOT at the site root. Moving it NARROWS the key to that directory only. |
Tell Google about a sitemap. `feedpath` is the sitemap's COMPLETE URL ("https://example.com/sitemap.xml"), never a path — Google addresses a sitemap by its full url. Re-submitting one that is already registered is harmless and simply re-notifies Google. THE ANSWER IS READ BACK FROM GOOGLE, not echoed from the request: it reports whether the sitemap is now actually registered, whether Google has processed it yet, and what errors and warnings it already carries. 0 credits.
| Argument | Type | Description |
|---|---|---|
| siteUrl* | string | the exact property string from list_search_console_sites |
| feedpath* | string | the FULL sitemap URL, e.g. "https://example.com/sitemap.xml" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"feedpath"
],
"properties": {
"siteUrl": {
"type": "string",
"description": "the exact property string from list_search_console_sites"
},
"feedpath": {
"type": "string",
"description": "the FULL sitemap URL, e.g. \"https://example.com/sitemap.xml\""
}
}
}Submit a business or individual verification request for a TikTok ad account or Business Center. IT DOES NOT UPLOAD ANY DOCUMENT AND HERMOSO NEVER HANDLES ONE: this sends the account details plus the ids of images the user already uploaded in TikTok Ads Manager, or the id of a document already on file that TikTok permits them to reuse; tiktok_ads_verification_status reports that id. A BUSINESS submission takes exactly one image id; an INDIVIDUAL one takes exactly two, the front and back of a single document. THE LEGAL NAME AND THE DOCUMENT NUMBER CANNOT BE CHANGED AFTERWARDS and must match the paperwork character for character, so this is confirm-gated: calling it without confirm sends nothing and returns a sentence naming exactly what would leave here. Submitting also needs real authority: TikTok requires admin or operator on an ad account, admin on a Business Center. Free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | Business Center id, for a Business Center verification; use instead of advertiserId |
| confirm | boolean | REQUIRED true: call without it first to see exactly what would be sent |
| licenseNo | string | BUSINESS: the certificate number, PERMANENT |
| identityNo | string | INDIVIDUAL: the government ID number, PERMANENT |
| websiteUrl | string | required for both |
| companyName | string | BUSINESS: the legal name, PERMANENT, must match the document exactly |
Push the leads the brand's LEAD FORMS collected into HubSpot as contacts — Meta, LinkedIn, TikTok (every published Instant Form, every lead region), X Ads, Snapchat and ChatGPT Ads (the last two: leads delivered to Hermoso by their lead webhooks). Each lead is upserted by email with lead status NEW and the platform + form + campaign written into the contact's Message property (a default HubSpot property, so no schema scope is needed). Synced lead ids are remembered per workspace, so a re-run, a scheduled run, a retry or a second agent never creates twins. Reports created / updated / already-synced / no-email per platform; a platform that is not connected, or cannot be read (Reddit has no API for leads), is REPORTED, not fatal. platforms narrows it. dryRun:true lists what would be synced and writes nothing. set_hubspot_lead_sync runs it automatically. Free. Needs HubSpot connected, plus the platforms whose leads you want.
| Argument | Type | Description |
|---|---|---|
| since | string | ISO date; default 7 days ago |
| dryRun | boolean | list what would be synced and write nothing |
| platforms | array | default every connected one |
| tiktokRegions | array | TikTok stores leads per region; default all three |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"since": {
"type": "string",
"description": "ISO date; default 7 days ago"
},
"dryRun": {
"type": "boolean",
"description": "list what would be synced and write nothing"
},
"platforms": {
"type": "array",
"items": {
"enum": [
"meta",
"linkedin",
"tiktok",
"x",
"snapchat",
"chatgpt",
"reddit"
],
"type": "string"
},
"description": "default every connected one"
},
"tiktokRegions": {
"type": "array",
"items": {
"enum": [
"us",
"eu",
"other"
],
"type": "string"
},
"description": "TikTok stores leads per region; default all three"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| leadFormId* | string | published lead form id |
| expectedPublishedRevisionId* | string | publishedRevisionId of the form, from get_openai_ads_lead_form |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"leadFormId",
"expectedPublishedRevisionId"
],
"properties": {
"leadFormId": {
"type": "string",
"description": "published lead form id"
},
"expectedPublishedRevisionId": {
"type": "string",
"description": "publishedRevisionId of the form, from get_openai_ads_lead_form"
}
}
}Clean up and consolidate the workspace Memory: drops entries that are about how Hermoso, a tool, a connector or a platform API behaves (product behaviour, not the brand), drops phone numbers and emails, and merges near-duplicate facts into one sentence each. Call with no argument to get the PROPOSAL (what would be removed and merged, with reasons) — nothing changes. Call again with confirm:true to apply it through the same typed writers the app uses (deletes carry tombstones so they stay deleted on every device). One model call, billed at its token cost.
| Argument | Type | Description |
|---|---|---|
| brand | string | profile id/name from list_brands, this call only |
| confirm | boolean | true to APPLY the proposal; omit to only see it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"confirm": {
"type": "boolean",
"description": "true to APPLY the proposal; omit to only see it"
}
}
}How much a TikTok audience overlaps with up to four others — the read that answers whether a new audience is genuinely new or is mostly the people you already target, which is what stops two ad groups bidding against each other for the same person. TIKTOK SUPPRESSES ANY AUDIENCE SIZE UNDER 1,000, so a missing or blank size here means "fewer than 1,000", never "nobody" — reporting it as a zero would be a confidently wrong answer. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| advertiserId | string | from list_tiktok_ads_accounts |
| benchmarkAudienceId* | string | the audience everything else is compared against |
| comparisonAudienceIds | array | up to 4 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"benchmarkAudienceId"
],
"properties": {
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"benchmarkAudienceId": {
"type": "string",
"description": "the audience everything else is compared against"
},
"comparisonAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 4"
}
}
}TikTok’s own diagnosis of a catalog — every detected issue at CRITICAL or WARNING level across product attributes, product review, catalog configuration, pixel/event problems and feed or file-upload failures, each carrying the reason AND TikTok’s suggested fix. This is the tool for "the feed ran and no products showed up" and for "my products were rejected and I do not know why". IT COVERS EVERY FEED IN THE CATALOG BY DEFAULT: naming a single feedId narrows it, and simply omitting the field at TikTok would silently report on the DEFAULT feed alone, so a multi-feed catalog with one broken feed would get a clean bill of health. Diagnostics regenerate once a day, so a fix made today may not clear until tomorrow’s run, and the reply says which day’s data it is. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | from list_tiktok_ads_business_centers |
| page | number | page number, from 1 |
| feedId | string | defaults to ALL feeds in the catalog |
| pageSize | number | up to 20 |
| catalogId* | string | from list_tiktok_ads_catalogs |
| issueLevel | string | CRITICAL or WARNING; omit for both |
| issueCategory |
The conversation around a single comment on a TikTok ad: give it an ORIGINAL comment and you get that comment plus every reply to it; give it a REPLY and you get the reply plus the comment it answers. Use it before replying to a heated thread, so you answer what was actually said rather than the one line list_tiktok_ads_comments showed. Pages up to 1000 at a time, which is ten times what the list read allows. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| pageSize | number | up to 1000 |
| commentId* | string | from list_tiktok_ads_comments |
| commentType | string | what THIS comment is — default COMMENT. The row from list_tiktok_ads_comments reports it. |
| advertiserId | string | from list_tiktok_ads_accounts |
| originalCommentId | string | only for a REPLY — the id of the comment it answers, as reported on the row |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"commentId"
],
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"pageSize": {
"type": "number",
"description": "up to 1000"
},
"commentId": {
"type": "string",
"description": "from list_tiktok_ads_comments"
},
"commentType": {
"enum": [
"COMMENT",
"REPLY"
],
"type": "string",
"description": "what THIS comment is — default COMMENT. The row from list_tiktok_ads_comments reports it."
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"originalCommentId": {
"type": "string",
"description": "only for a REPLY — the id of the comment it answers, as reported on the row"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| endDate | string | YYYY-MM-DD (default today) |
| adgroupId* | string | a Smart+ ad group (list_tiktok_smart_campaigns) |
| startDate | string | YYYY-MM-DD, within the last 14 days (default 7 days ago) |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| adMaterialIds | array | optional; default every creative in the ad group |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adgroupId"
],
"properties": {
"endDate": {
"type": "string",
"description": "YYYY-MM-DD (default today)"
},
"adgroupId": {
"type": "string",
"description": "a Smart+ ad group (list_tiktok_smart_campaigns)"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD, within the last 14 days (default 7 days ago)"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"adMaterialIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "optional; default every creative in the ad group"
}
}
}TikTok’s issues-and-suggestions diagnosis for the ad groups on an ad account — its own read on why delivery is underperforming, in three categories: CREATIVE (no background music, video too short, resolution too low), BID_AND_BUDGET (a suggested bid or budget, a recommendation to switch to Maximum Delivery, and the full Estimated Delivery Results tables pairing nine bid levels or fifteen budget levels with their estimated cost, conversions, CPA and impressions), and EVENT_TRACK (a pixel that has recorded nothing for seven days, which means the ad group is optimising toward an event nothing is firing). Every issue code comes back with a plain sentence saying what to DO about it. Warning: AN EMPTY ANSWER IS NOT A CLEAN BILL OF HEALTH: TikTok diagnoses ACTIVE ad groups only and omits any ad group it has no suggestions for, so a missing ad group is EITHER healthy OR not currently active and TikTok does not distinguish the two. At most 20 ad groups per call. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| adgroupIds | array | up to 20 — omit for every active ad group on the account |
| advertiserId | string | from list_tiktok_ads_accounts |
| issueCategories | array | omit for all three. An unknown category is refused rather than dropped, so you never get an answer about something other than what you asked for. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adgroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 20 — omit for every active ad group on the account"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"issueCategories": {
"type": "array",
"items": {
"enum": [
"CREATIVE",
"BID_AND_BUDGET",
"EVENT_TRACK"
],
"type": "string"
},
"description": "omit for all three. An unknown category is refused rather than dropped, so you never get an answer about something other than what you asked for."
}
}
}Event-level statistics for one TikTok pixel over a date range — how many times each event actually fired, which is how you tell a pixel that is installed and working from one that is installed and silent. Pass the pixelId or the pixelCode from list_tiktok_ads_pixels; default range is the last 7 days (UTC), at most 30. Counts are split into browser vs server per event. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| endDate | string | YYYY-MM-DD (UTC); default today |
| pixelId | string | the pixelId from list_tiktok_ads_pixels |
| pixelCode | string | or the pixelCode from list_tiktok_ads_pixels — resolved to its id |
| startDate | string | YYYY-MM-DD (UTC); default 6 days before endDate; the range may span at most 30 days |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"endDate": {
"type": "string",
"description": "YYYY-MM-DD (UTC); default today"
},
"pixelId": {
"type": "string",
"description": "the pixelId from list_tiktok_ads_pixels"
},
"pixelCode": {
"type": "string",
"description": "or the pixelCode from list_tiktok_ads_pixels — resolved to its id"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD (UTC); default 6 days before endDate; the range may span at most 30 days"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}Use this when the user asks how their TikTok ads performed. Performance for a TikTok advertiser account at campaign, ad group, ad or whole-advertiser level — spend, impressions, clicks, CTR, CPC, CPM and conversions by default, or whichever of TikTok’s metrics and grouping dimensions you name. `reportType` picks WHICH REPORT: BASIC is spend and performance (the default); AUDIENCE is the same money broken down by WHO SAW IT — pass an audience dimension such as age, gender, country_code or interest_category, which BASIC refuses outright. 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, spends nothing. NOTE: everything Hermoso creates on TikTok is created PAUSED, so a brand-new build reports nothing at all until somebody activates it with set_tiktok_ads_status(confirm:true) — check the statuses with list_tiktok_ads_campaigns before reading an empty report as bad performance.
| Argument | Type | Description |
|---|---|---|
| page | number | 1-based page (default 1). A report LONGER than one page comes back with hasMore:true and a truncationNote — totals computed from one page are WRONG. |
| level | string | the reporting level — default campaign. "advertiser" is the whole account, which is the level an audience breakdown is normally run at. |
| endDate | string | YYYY-MM-DD |
| metrics | array | TikTok metric names — default ["spend","impressions","clicks","ctr","cpc","cpm","conversion"] |
| pageSize |
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.
| Argument | Type | Description |
|---|---|---|
| adIds* | array | 1-100 ad ids from list_tiktok_ads_campaigns |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adIds"
],
"properties": {
"adIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "1-100 ad ids from list_tiktok_ads_campaigns"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Reach & Frequency INVENTORY ESTIMATE — how many people TikTok can reach, for what budget, at what frequency, over a fixed window, before anything is booked. THIS IS THE ONLY SANCTIONED SOURCE of the three numbers create_tiktok_ads_rf_ad_group needs (budget, purchasedImpression, purchasedReach), and that matters because TikTok does NOT refuse a reservation outside the range it estimated — it silently books its own maximum instead, so an invented number buys something nobody asked for. IMPRESSIONS AND REACH COME BACK IN THOUSANDS (110 means 110,000) and the create takes the same unit, so pass them back UNSCALED. Fix ONE of the three with rfPurchasedType (FIXED_BUDGET / FIXED_SHOW / FIXED_REACH) and TikTok predicts the other two; omit it to get the whole range. Reach & Frequency is ALLOWLIST-ONLY per ad account. Read-only, free, books nothing.
| Argument | Type | Description |
|---|---|---|
| budget | number | required with FIXED_BUDGET |
| gender | string | TikTok gender value; omit to reach everyone |
| feedType | string | STANDARD_FEED (large inventory, normal CPM) or TOP_FEED (limited inventory, higher CPM) |
| ageGroups | array | TikTok age band values; omit to reach all ages |
| frequency* | number | with frequencySchedule, the cap: frequency 2 + frequencySchedule 3 means "at most twice every 3 days". Must be <= frequencySchedule and never more than 4 per day. |
EVERY EXECUTION OF THE USER’S AUTOMATED RULES — when each ran, what it acted on and what it changed. THIS IS THE ONLY PLACE THEY WILL SEE IT: TikTok routes rule notification emails to the developer address registered on the app rather than to the advertiser, so an armed rule can pause campaigns or raise budgets on their account and nothing reaches their inbox. Call it with no ids to list executions (optionally filtered by rule, action, dimension or a UTC time window); pass ruleId AND execId TOGETHER, both taken from one row of that list, for the detail of a single run — TikTok keys a result on the pair, so half of one is refused rather than answered about the wrong thing. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| time | array | exactly two UTC datetimes: start and end |
| action | string | filter to one kind of action — use this to answer "has anything raised my budgets?" |
| execId | string | for the DETAIL of one run — must be sent with ruleId from the same row |
| ruleId | string | for the DETAIL of one run — must be sent with execId from the same row |
| status | string | ON, OFF or DELETED; status of the rules whose runs are listed |
| pageSize |
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.
| Argument | Type | Description |
|---|---|---|
| kind | string | default hashtags |
| category | string | industry category, default ALL |
| dateRange | string | trend window: 1DAY, 7DAY, 30DAY or 120DAY |
| hashtagIds | array | for hashtag_detail (first id) and hashtag_videos (up to 10) |
| countryCode | string | 2-letter, default US |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"hashtags",
"hashtag_detail",
"hashtag_videos"
],
"type": "string",
"description": "default hashtags"
},
"category": {
"type": "string",
"description": "industry category, default ALL"
},
"dateRange": {
"enum": [
"1DAY",
"7DAY",
"30DAY",
"120DAY"
],
"type": "string",
"description": "trend window: 1DAY, 7DAY, 30DAY or 120DAY"
},
"hashtagIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "for hashtag_detail (first id) and hashtag_videos (up to 10)"
},
"countryCode": {
"type": "string",
"description": "2-letter, default US"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}Whether a TikTok ad account or Business Center has passed business verification, when it was reviewed, and if it failed, TikTok’s own reason. AN AD ACCOUNT AND A BUSINESS CENTER ARE VERIFIED SEPARATELY and carry different statuses, so pass one or the other and never both; a verified Business Center does not verify the ad accounts inside it. Worth reading during onboarding: an unverified account meets limits that otherwise get diagnosed as something else entirely. It also reports the qualification id of any document already on file, which submit_tiktok_ads_verification can reuse. Read-only, free.
| Argument | Type | Description |
|---|---|---|
| bcId | string | a Business Center: pass this OR advertiserId, not both |
| advertiserId | string | an ad account: pass this OR bcId, not both |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bcId": {
"type": "string",
"description": "a Business Center: pass this OR advertiserId, not both"
},
"advertiserId": {
"type": "string",
"description": "an ad account: pass this OR bcId, not both"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adIds | array | ad ids to read (AD report) |
| videoId | string | VIDEO report: one video id from upload_tiktok_ads_creative |
| adgroupIds | array | ad group ids to read (AD report) |
| materialId | string | creative material id to read one specific material |
| reportType | string | AD (default) reads ads by adIds, adgroupIds or campaignIds; VIDEO reads one video by videoId |
| campaignIds | array | campaign ids to read (AD report) |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
Whether TikTok's BID PROTECTION is covering a Smart+ campaign or ad group, and what it actually paid back. Bid protection compensates in AD CREDITS when delivery misses the bid, so this answers two different questions from one tool: with no dates it returns today's coverage STATUS per id; with startDate and endDate it returns the day-by-day HISTORY, each row carrying the credit amount and currency. Smart+ objects ONLY — a classic TikTok campaign id does not resolve here. TikTok keeps 60 days, and the two endpoints size differently: status takes up to 200 ids, history allows 200 DIVIDED BY the day span (a 10-day window takes 20 ids), which is refused here with the arithmetic done rather than by TikTok with it left to you. An id TikTok says nothing about is NAMED in the reply, because a short list otherwise reads as 'those are unprotected'. Read-only, 0 credits.
| Argument | Type | Description |
|---|---|---|
| ids* | any | Smart+ ids from list_tiktok_smart_campaigns |
| level* | string | which Smart+ level the ids are |
| endDate | string | YYYY-MM-DD, on or after startDate and within 60 days of it |
| startDate | string | YYYY-MM-DD in the ad account timezone. With endDate this returns HISTORY instead of status; must be within the last 60 days |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| labelId* | string | from list_tiktok_creator_labels(labelType:"RANKING"), matching rankingType |
| lookback* | string | ONE is the most recent completed period |
| pageSize | number | 1-100, default 20 |
| timePeriod* | string | ranking period: WEEK or MONTH |
| countryCode | string | US: the only country TikTok ranks |
| rankingType |
TikTok’s Lead Generation Ads terms for an ad account — whether they are accepted, and the link to read them. TikTok refuses every Instant-Form lead ad (create_tiktok_ads_ad with pageId) until the ad account has accepted them. Without confirm this only READS and changes nothing. Accepting is the ADVERTISER’s legal act: show the user the terms link and pass confirm:true ONLY after they say they accept. The result is read back from TikTok. Free.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | true = accept the terms on the user’s behalf — only after they have seen the link and said yes |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"confirm": {
"type": "boolean",
"description": "true = accept the terms on the user’s behalf — only after they have seen the link and said yes"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| time | string | PAST_ONE_MONTH, PAST_THREE_MONTHS or PAST_SIX_MONTHS |
| videoIds | array | rate only these (up to 20) |
| objective | string | campaign objective to rate posts for, e.g. REACH, TRAFFIC or VIDEO_VIEWS; omit for all |
| ttoAccountId | string | rate TikTok One creator posts instead of your own |
| excludeVideoIds | array | post ids to leave out of the ratings |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"time": {
"enum": [
"PAST_ONE_MONTH",
"PAST_THREE_MONTHS",
"PAST_SIX_MONTHS"
],
"type": "string",
"description": "PAST_ONE_MONTH, PAST_THREE_MONTHS or PAST_SIX_MONTHS"
},
"videoIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "rate only these (up to 20)"
},
"objective": {
"type": "string",
"description": "campaign objective to rate posts for, e.g. REACH, TRAFFIC or VIDEO_VIEWS; omit for all"
},
"ttoAccountId": {
"type": "string",
"description": "rate TikTok One creator posts instead of your own"
},
"excludeVideoIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "post ids to leave out of the ratings"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| kind* | string | sounds (trending tracks), sound_videos (videos using soundId), search_terms, or search_suggestions (needs query) |
| genre | string | music genre for kind sounds, e.g. POP or HIP_HOP/RAP, or ALL |
| query | string | for search_suggestions |
| soundId | string | for sound_videos — soundId from kind sounds |
| dateRange | string | trend window: 1DAY, 7DAY, 30DAY or 90DAY |
| countryCode | string | 2-letter country code the trend data is for, e.g. US |
| personalized | boolean | search terms personalized for this account |
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.
| Argument | Type | Description |
|---|---|---|
| page | number | page number, from 1 |
| endDate | string | YYYY-MM-DD (UTC+0), paired with startDate |
| pageSize | number | 1-100, default 25 |
| startDate | string | YYYY-MM-DD (UTC+0), paired with endDate: together they add per-day figures |
| campaignId* | string | TikTok One campaign id from list_tiktok_tto_campaigns |
FIRE one of the actions merchant_issue_help returned — most usefully 'request a review' on a disapproval. SEVERAL OF THESE ARE ONE-SHOT and Google says so in its own dialog callout ('You can only request a review for disagreeing with this issue once. If it's not approved, you'll need to fix the issue and wait'), so read that callout back to the user before confirming: a review spent on the wrong issue cannot be spent again. WITHOUT `confirm` IT FIRES NOTHING. `actionContext` is OPAQUE by Google's own instruction — pass it back exactly as merchant_issue_help gave it; it cannot be constructed or edited. The answer is Google's own message to the business, relayed verbatim, because this endpoint returns no resource to read back. 0 credits.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually fire it |
| inputValues | array | values for the flow's input fields, shaped as Google described them: {inputFieldId, textInputValue|choiceInputValue|checkboxInputValue} |
| actionFlowId* | string | the id of the flow you picked from that action's flows — each asks for different inputs |
| languageCode | string | BCP-47 language for the response message |
| actionContext* | string | the opaque token from merchant_issue_help, on the action you are firing |
Remove a TikTok post from this ad account’s authorised Spark Ads list — the cleanup for an authorization that has expired or been revoked. IT IS NOT REVERSIBLE FROM OUR SIDE: re-authorising needs a FRESH code from the post’s owner, who is usually not the person making this call, so getting it wrong costs a conversation with a creator rather than a second API call — which is why it is confirm-gated even though it spends nothing. Ads already built on the post keep running; what is lost is the ability to build a NEW one. Calling it without confirm CHANGES NOTHING and returns the sentence describing exactly what would happen. THE READ-BACK INVERTS: the proof is the post’s ABSENCE from the authorised list afterwards, and an id that is still there is reported as not confirmed rather than done.
| Argument | Type | Description |
|---|---|---|
| itemId* | string | from list_tiktok_ads_spark_posts |
| confirm | boolean | REQUIRED true — call without it first to see exactly what would change |
| advertiserId | string | TikTok advertiser id from list_tiktok_ads_accounts; omit when only one is connected |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"itemId"
],
"properties": {
"itemId": {
"type": "string",
"description": "from list_tiktok_ads_spark_posts"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — call without it first to see exactly what would change"
},
"advertiserId": {
"type": "string",
"description": "TikTok advertiser id from list_tiktok_ads_accounts; omit when only one is connected"
}
}
}REMOVE the link between a GA4 property and a Google Ads account. The link itself is trivially re-creatable, which is why this is not a one-way-door gate — but what breaks downstream is not: every campaign bidding on a conversion imported from this property loses that signal, and every remarketing list built from a GA4 audience stops refreshing. WITHOUT `confirm` IT CHANGES NOTHING and reports what is actually attached, read back from Google, including whether it is a MANAGER account (in which case the link covers every account underneath). 0 credits.
| Argument | Type | Description |
|---|---|---|
| confirm | boolean | must be true to actually unlink — without it nothing changes and the link is described back to you |
| property* | string | the NUMERIC GA4 property id |
| customerId* | string | the Google Ads customer id, from list_analytics_google_ads_links |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"customerId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually unlink — without it nothing changes and the link is described back to you"
},
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id"
},
"customerId": {
"type": "string",
"description": "the Google Ads customer id, from list_analytics_google_ads_links"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| id | string | Defaults to the ad account this connection resolved. |
| name | string | new ad account name |
| delegations | array | REPLACES the entire set. [{"resourceType":"CONTENT_PROVIDER","resourceId":"<CPID>"}]. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Defaults to the ad account this connection resolved."
},
"name": {
"type": "string",
"description": "new ad account name"
},
"delegations": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "REPLACES the entire set. [{\"resourceType\":\"CONTENT_PROVIDER\",\"resourceId\":\"<CPID>\"}]."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| id* | string | REQUIRED. |
| name | string | new budget order name |
| amount | string | New total, e.g. "75000.00". Raise it to extend a budget nearing exhaustion. |
| endTime | string | Can only SHORTEN an active budget order’s window. |
| currency | string | ISO 4217 currency code |
| openEnded | boolean | true clears the end date entirely, making the budget open-ended. This is the only way to extend rather than shorten. |
| startTime | string |
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.
| Argument | Type | Description |
|---|---|---|
| id* | string | REQUIRED. |
| bid | string | Keyword only. |
| name | string | new name for the campaign, ad group, creative, ad or location group |
| level |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | new campaign name |
| confirm | boolean | required for a spend change on a LIVE campaign |
| endDate | string | YYYY-MM-DD |
| clickUrl | string | click-tracking URL for app campaigns |
| usMetros | array | US metro names such as NEW_YORK; not together with usRegions |
| countries | array | ISO country codes the campaign runs in |
| goalValue | number | new target cost, or ROAS ratio (0.9 = 90%) |
| usRegions | array | US state codes; only when US is a target country |
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.
| Argument | Type | Description |
|---|---|---|
| url | string | landing page for this set; "" = the campaign’s |
| name | string | new creative set name |
| confirm | boolean | required to add a LIVE set to a LIVE campaign |
| assetIds | array | REPLACES the set’s assets |
| brandInfo | object | override the account brand on THIS creative set — how a brand A/B tests the end card |
| countries | array | ISO codes; [] = all countries |
| languages | array | e.g. ENGLISH; [] = all |
| creativeSetId |
Patch SPECIFIC fields of the active profile (name, domain, sells, summary, category, audience, positioning, voice, style, goal, logos, brand/product name pronunciations, profileType: explore = kept but unused) WITHOUT overwriting the rest — a read-modify-write on the saved brand. Use for “change our voice to playful”, “we sell to dentists now”. To onboard a brand from scratch, use draft_brand. If no brand is saved yet and you only INFERRED one from what the user is making, ask them to confirm it is their brand before saving it (they may be working for a client or just trying things). Only pass the fields you’re changing.
| Argument | Type | Description |
|---|---|---|
| goal | string | current marketing goal |
| logo | string | logo image: URL or local file path to a PNG, JPEG or WebP (no SVG) |
| name | string | brand name |
| brand | string | profile id/name from list_brands, this call only |
| sells | string | what the brand sells |
| style | string | visual style — palette, typography, aesthetic |
| voice | string | brand voice/tone |
| domain | string |
EDIT a Google Doc — the correction append_to_doc cannot make. Two shapes: `replacements:[{find, replace}]` rewrites specific text wherever it appears (call read_doc first and match the text EXACTLY; matchCase:false ignores case), or `rewrite:"…"` replaces the ENTIRE body (rewrite:"" empties it), or `dropdowns:[{title, value}]` sets a dropdown chip (e.g. Status -> Approved — read_doc lists every chip with its options; pass dropdownId when two share a title; an unknown title or option is refused with the real list and nothing changes); the same entry with options:[…] (the COMPLETE new list, 2–50) and/or newTitle changes the dropdown itself for every chip built from it, and an option you drop that a chip shows needs replace:{"Old":"New"}. Find/replace runs immediately and REPORTS how many occurrences changed — zero matches is reported as a FAILURE to match, never as a quiet success, because a text edit that silently does nothing is worse than one that visibly fails. A whole-body rewrite is destructive: call it without confirm first to get the character count, then confirm:true + confirmCells. Both are index-free by design — an agent cannot reliably compute Google’s character offsets, and a wrong offset deletes the wrong sentence.
| Argument | Type | Description |
|---|---|---|
| docUrl | string | a Google Docs URL — the id is extracted from it |
| confirm | boolean | true runs a whole-body rewrite after the character count is shown |
| rewrite | string | replace the WHOLE body with this text ("" empties the doc) |
| dropdowns | array | dropdown chips to set, or dropdowns to change |
Update a Drive file: rename (name), move it into a folder (moveToFolderId, optionally removeFromFolderId to move OUT of the old one), or trash / untrash it (trash:true|false). Pass fileId (from list_drive_files). To delete permanently, use delete_drive_file.
| Argument | Type | Description |
|---|---|---|
| name | string | new name |
| trash | boolean | true -> move to Trash; false -> restore from Trash |
| fileId* | string | the Drive file id |
| moveToFolderId | string | folder id to move the file into (from create_drive_folder / list_drive_files) |
| removeFromFolderId | string | the old parent folder id to remove (when moving) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"name": {
"type": "string",
"description": "new name"
},
"trash": {
"type": "boolean",
"description": "true -> move to Trash; false -> restore from Trash"
},
"fileId": {
"type": "string",
"description": "the Drive file id"
},
"moveToFolderId": {
"type": "string",
"description": "folder id to move the file into (from create_drive_folder / list_drive_files)"
},
"removeFromFolderId": {
"type": "string",
"description": "the old parent folder id to remove (when moving)"
}
}
}Edit a LinkedIn ad (creative) in place: its copy (text), landing page, button (callToAction), its name, or — while it is still DRAFT — the lead gen form it opens (leadFormId) and that button’s label (leadButton). Copy, landing page and button live on the ad’s sponsored post and are edited there. An ACTIVE ad needs confirm:true because real people see the change on the next impression, and LinkedIn may review it again. Status is NOT changed here — use set_linkedin_ads_status. Everything is read back from LinkedIn. Free.
| Argument | Type | Description |
|---|---|---|
| name | string | creative name shown in Campaign Manager |
| text | string | new ad copy |
| confirm | boolean | REQUIRED true to edit an ACTIVE ad |
| creativeId* | string | from list_linkedin_ads_campaigns(campaignId) or create_linkedin_ads_creative |
| leadButton | string | lead gen button label; only while the ad is DRAFT |
| leadFormId | string | DRAFT creatives only — the lead gen form the ad opens |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
Rename a LinkedIn conversion rule, change its attribution windows or attribution model, or ENABLE/DISABLE it. Disabling is how a conversion rule is retired — LinkedIn publishes no delete for one, and while it is disabled every streamed event for it is refused. The rule’s `type` is immutable. The note quotes what LinkedIn returned on the read-back, not what was sent — repeat that. Free.
| Argument | Type | Description |
|---|---|---|
| name | string | rule name shown in Campaign Manager and reports |
| enabled | boolean | false RETIRES it — streamed events are then refused |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
| conversionId* | string | the rule id, e.g. "104012" |
| attributionType | string | how conversions are credited across campaigns: last touch by campaign or by conversion |
| postClickAttributionWindowSize | number | post-click window in days: 1, 7, 30 or 90 (365 for the long-window types) |
| viewThroughAttributionWindowSize | number | view-through window in days: 1, 7, 30 or 90 (365 for the long-window types) |
Edit a LinkedIn lead gen form — name, headline, description, questions, privacy policy, disclaimer, consents, thank-you message / button / landing page — or retire it with state:"ARCHIVED". LinkedIn allows every field while no running ad uses the form; once a live ad does, only headline, description, disclaimer, privacy policy, the thank-you fields and state can change, and anything else is refused here by name. Changing the QUESTIONS can break a CRM that already mapped the form, so tell the user. Read back from LinkedIn. Free.
| Argument | Type | Description |
|---|---|---|
| name | string | form name shown in Campaign Manager |
| state | string | PUBLISHED keeps the form live; ARCHIVED retires it |
| formId* | string | form id |
| pageId | string | Omit when one Page is shared |
| consents | array | consent checkbox texts, or {text, required: true} for a mandatory one |
| headline | string | title of the form people see |
| questions | array | the fields to ask, in order: predefined names LinkedIn prefills from the member profile (FIRST_NAME, LAST_NAME, EMAIL, WORK_EMAIL, PHONE_NUMBER, COMPANY_NAME, JOB_TITLE, SENIORITY, COMPANY_SIZE, INDUSTRY, CITY, COUNTRY, ZIP_CODE, LINKEDIN_PROFILE_LINK …; friendly spellings like "work email" work) and/or custom questions {question, type:"text", maxLength?} or {question, options:[2–15 choices]} |
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.
| Argument | Type | Description |
|---|---|---|
| mpn | string | manufacturer part number |
| gtin | string | GTIN barcode (UPC, EAN or similar) |
| link | string | product page URL on the merchant's store |
| size | string | product size |
| brand | string | brand name |
| color | string | product colour |
| price | object | new price: {value in major units or amountMicros, currencyCode} |
| title | string |
Update an EXISTING campaign, ad set, or ad — rename, change its daily budget, retarget (ad sets), or change status (PAUSED / ACTIVE / ARCHIVED). Pass objectId (from list_meta_ads) + adAccountId. Setting something ACTIVE can start REAL AD SPEND — show the user what will run + its budget, get a yes, then pass confirm:true. Pausing and renaming are always safe and always reversible. ARCHIVING IS NOT: Meta treats an archived object as DELETED and refuses to bring it back — every later edit answers "This campaign has been deleted, so you can only edit the name" (measured live 2026-08-05), archiving a campaign takes its ad sets and ads down with it, and the only way back is to duplicate it as a new object. Use PAUSED unless the user has said they are finished with it for good.
| Argument | Type | Description |
|---|---|---|
| name | string | new name |
| status | string | ACTIVE starts spend (needs confirm:true); PAUSED / ARCHIVED are safe |
| confirm | boolean | REQUIRED true ONLY to set status ACTIVE (real spend) |
| dsaPayor | string | ad sets: EU ad transparency, who pays (defaults to dsaBeneficiary) |
| objectId* | string | the campaign / ad set / ad id (from list_meta_ads) |
| targeting | object | replacement targeting spec (ad sets) — a Meta targeting object |
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.
| Argument | Type | Description |
|---|---|---|
| tags | array | annotation-tag ids |
| description | string | new annotation text; empty text is refused |
| annotationId* | number | from list_mixpanel_annotations |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"annotationId"
],
"properties": {
"tags": {
"type": "array",
"items": {
"type": "number"
},
"description": "annotation-tag ids"
},
"description": {
"type": "string",
"description": "new annotation text; empty text is refused"
},
"annotationId": {
"type": "number",
"description": "from list_mixpanel_annotations"
}
}
}Change properties on ONE row of a Notion database (pageId from query_notion_database) — e.g. mark a calendar row Status "Posted" and put the live post link in its URL column. properties is {"Column": value} by column name: text, number, checkbox, select (an existing option or a new one), status (must be an existing option), multi-select (array; replaces the whole value), date (ISO, or {start, end}), url, email, phone; null clears. Validated against the database's own columns before anything is sent and refused by name otherwise, never coerced; columns you do not name keep their values. Read back from Notion. trash:true moves the row to Notion's trash (restorable; returns a preview first and needs confirm:true), trash:false restores it. Free.
| Argument | Type | Description |
|---|---|---|
| trash | boolean | true moves the row to Notion trash (needs confirm); false restores it |
| pageId* | string | the row id or link |
| confirm | boolean | true confirms moving the row to trash; not needed for other changes |
| properties | object | {"Column": value} |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pageId"
],
"properties": {
"trash": {
"type": "boolean",
"description": "true moves the row to Notion trash (needs confirm); false restores it"
},
"pageId": {
"type": "string",
"description": "the row id or link"
},
"confirm": {
"type": "boolean",
"description": "true confirms moving the row to trash; not needed for other changes"
},
"properties": {
"type": "object",
"description": "{\"Column\": value}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}Update a OneDrive item: rename (name) and/or move it into a folder (moveToFolderId). Pass fileId (from list_onedrive_files). To remove an item, use delete_onedrive_file.
| Argument | Type | Description |
|---|---|---|
| name | string | new name |
| fileId* | string | the OneDrive item id |
| moveToFolderId | string | folder id to move the item into (from create_onedrive_folder / list_onedrive_files) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"name": {
"type": "string",
"description": "new name"
},
"fileId": {
"type": "string",
"description": "the OneDrive item id"
},
"moveToFolderId": {
"type": "string",
"description": "folder id to move the item into (from create_onedrive_folder / list_onedrive_files)"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| url | string | the account’s primary destination — a public http(s) URL. OpenAI accepts this field even though their published parameter table omits it. |
| name | string | legacy: sets all three names |
| confirm | boolean | required for a brand or legal change |
| brandName | string | shown in ads; pauses delivery until review (confirm) |
| legalName | string | restarts business review (confirm) |
| faviconUrl | string |
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.
| Argument | Type | Description |
|---|---|---|
| members* | array | emails and/or phone numbers (already-SHA256-hashed emails pass through as-is) |
| operation* | string | add | remove | replace — replace swaps the entire membership |
| audienceId* | string | ChatGPT Ads audience id, from list_openai_ads_audiences |
| expectedRevision |
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.
| Argument | Type | Description |
|---|---|---|
| feedId* | string | the product feed already linked to this ad account |
| products* | array | [{id, variants:[{id, title?, price?:{amount:int minor units, currency}, availability?:{available:bool}|{status:"in_stock"|"out_of_stock"}}]}] |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"feedId",
"products"
],
"properties": {
"feedId": {
"type": "string",
"description": "the product feed already linked to this ad account"
},
"products": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "[{id, variants:[{id, title?, price?:{amount:int minor units, currency}, availability?:{available:bool}|{status:\"in_stock\"|\"out_of_stock\"}}]}]"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name | string | new feed name |
| feedId* | string | product feed id to edit |
| searchEligibleRespected | boolean | true = the organic ChatGPT listing follows each product's is_eligible_search flag |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"feedId"
],
"properties": {
"name": {
"type": "string",
"description": "new feed name"
},
"feedId": {
"type": "string",
"description": "product feed id to edit"
},
"searchEligibleRespected": {
"type": "boolean",
"description": "true = the organic ChatGPT listing follows each product's is_eligible_search flag"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name* | string | lead form name; the whole definition is replaced |
| fields* | array | all form questions in order; replaces the whole field list |
| leadFormId* | string | lead form id to edit |
| privacyPolicyUrl | string | privacy policy URL shown with the form |
| expectedDraftRevisionId* | string | draftRevisionId from the last read; refused if stale |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"leadFormId",
"expectedDraftRevisionId",
"name",
"fields"
],
"properties": {
"name": {
"type": "string",
"description": "lead form name; the whole definition is replaced"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"required": [
"fieldId",
"fieldType",
"label"
],
"properties": {
"label": {
"type": "string",
"description": "question text shown to the person"
},
"fieldId": {
"type": "string",
"description": "your stable key for the field, e.g. email"
},
"options": {
"type": "array",
"items": {
"type": "string"
},
"description": "answer choices; choice fields only"
},
"required": {
"type": "boolean",
"description": "true = the person must answer this question"
},
"fieldType": {
"enum": [
"text",
"choice"
],
"type": "string",
"description": "text, or choice (choice needs options)"
}
}
},
"description": "all form questions in order; replaces the whole field list"
},
"leadFormId": {
"type": "string",
"description": "lead form id to edit"
},
"privacyPolicyUrl": {
"type": "string",
"description": "privacy policy URL shown with the form"
},
"expectedDraftRevisionId": {
"type": "string",
"description": "draftRevisionId from the last read; refused if stale"
}
}
}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 cost target, or demoting a maximize_* / target_* bid strategy. The reply says if ChatGPT Ads stored a different bid than was sent, and flags a Max Bid / Max CPC ad group (retired 2027-01-01). 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.
| Argument | Type | Description |
|---|---|---|
| adId | string | ad id to edit; use with level ad |
| name | string | new name |
| level | string | inferred from which id you pass |
| maxBid | number | DEPRECATED — Max Bid / Max CPC, retired by ChatGPT Ads on 2027-01-01: a fixed bid in the account currency (bidStrategy fixed_bid; on a conversions campaign it is the CPA bid). Needed only on an impressions campaign (per impression); otherwise use intelligent bidding or a target. |
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.
| Argument | Type | Description |
|---|---|---|
| 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 |
| adId* | string | the ad to edit |
| name | string | new ad name |
| pinId | string | DRAFT ads only — Pinterest refuses it on any other status |
| status | string | REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete |
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.
| Argument | Type | Description |
|---|---|---|
| bid | number | what you pay per billable event, in the ad account’s currency |
| name | string | new ad group name |
| budget | number | only on a campaign that is NOT budget-optimized |
| pacing | string | delivery speed: STANDARD or ACCELERATED |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | new audience name |
| 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 |
| audienceId* | string | id of the audience to rename, edit or remove |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| description | string | new audience description |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"name": {
"type": "string",
"description": "new audience name"
},
"rule": {
"type": "object",
"description": "raw Pinterest rule fields merged onto the audience's existing rule (expert use)",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"remove": {
"type": "boolean",
"description": "true removes the audience; requires confirm:true"
},
"confirm": {
"type": "boolean",
"description": "true required together with remove:true"
},
"audienceId": {
"type": "string",
"description": "id of the audience to rename, edit or remove"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"description": {
"type": "string",
"description": "new audience description"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| maids | array | mobile ad ids to add or remove (see operation); hashed on our server |
| emails | array | email addresses to add or remove (see operation); hashed on our server |
| operation | string | default ADD |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| customerListId* | string | id of the customer list to change |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"customerListId"
],
"properties": {
"maids": {
"type": "array",
"items": {
"type": "string"
},
"description": "mobile ad ids to add or remove (see operation); hashed on our server"
},
"emails": {
"type": "array",
"items": {
"type": "string"
},
"description": "email addresses to add or remove (see operation); hashed on our server"
},
"operation": {
"enum": [
"ADD",
"REMOVE"
],
"type": "string",
"description": "default ADD"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"customerListId": {
"type": "string",
"description": "id of the customer list to change"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| bid | number | new bid in the ad account currency |
| status | string | ACTIVE, PAUSED or ARCHIVED |
| confirm | boolean | true required when activating or changing the bid under a live campaign |
| included | boolean | false excludes the product group from the ad group |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| promotionId* | string | promotion id from list_pinterest_ads_product_group_promotions |
| trackingUrl | string | new tracking URL for clicks |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"promotionId"
],
"properties": {
"bid": {
"type": "number",
"description": "new bid in the ad account currency"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED"
],
"type": "string",
"description": "ACTIVE, PAUSED or ARCHIVED"
},
"confirm": {
"type": "boolean",
"description": "true required when activating or changing the bid under a live campaign"
},
"included": {
"type": "boolean",
"description": "false excludes the product group from the ad group"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"promotionId": {
"type": "string",
"description": "promotion id from list_pinterest_ads_product_group_promotions"
},
"trackingUrl": {
"type": "string",
"description": "new tracking URL for clicks"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| adId* | 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 |
| previewOff | boolean | true switches the preview link off |
| productIds | array | catalog ads: up to 6 featured product ids |
| adAccountId | string |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | new ad group name |
| budget | number | new budget in the ad account currency, paired with goalType |
| bidType | string | CPC, CPM, CPV, CPV6 or CPV15; must fit the campaign objective |
| endTime | string | ISO 8601 new end time |
| goalType | string | DAILY_SPEND or LIFETIME_SPEND |
| schedule | array | weekly delivery windows: startDay, endDay, startHour, endHour (0 = Sunday) |
| adGroupId* | string |
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.
| Argument | Type | Description |
|---|---|---|
| users* | array | up to 2,500 rows; every row must carry the same fields |
| action | string | default ADD |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| customAudienceId* | string | from create_reddit_ads_audience or list_reddit_ads_audiences |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"customAudienceId",
"users"
],
"properties": {
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"maid": {
"type": "string",
"description": "IDFA (uppercase hex, dashes) or AAID (lowercase hex, dashes), or a 64-char SHA-256 hash"
},
"email": {
"type": "string",
"description": "plain address or a 64-char SHA-256 hash"
}
}
},
"description": "up to 2,500 rows; every row must carry the same fields"
},
"action": {
"enum": [
"ADD",
"REMOVE"
],
"type": "string",
"description": "default ADD"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"customAudienceId": {
"type": "string",
"description": "from create_reddit_ads_audience or list_reddit_ads_audiences"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| name | string | new campaign name |
| budget | number | new campaign budget in the ad account currency |
| endTime | string | ISO 8601 new flight end |
| goalType | string | DAILY_SPEND or LIFETIME_SPEND |
| spendCap | number | lifetime spend ceiling in the ad account’s currency |
| startTime | string | ISO 8601 new flight start |
| campaignId* | string | id of the campaign to edit |
| adAccountId | string |
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.
| Argument | Type | Description |
|---|---|---|
| id* | string | the catalog, feed or product set id |
| url | string | feed only |
| kind* | string | catalog, feed or product_set: which type the id refers to |
| mode | string | feed only |
| name | string | new name |
| filter | any | product_set only — the new rule |
| password | string | feed only |
| pixelIds | array | catalog only — replaces the attached pixels |
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.
| Argument | Type | Description |
|---|---|---|
| adId* | string | template ad id, from create_reddit_ads_max_campaign |
| name | string | new template ad name |
| assetIds | array | REPLACES the template’s assets |
| displayUrl | string | new display URL; same domain as destinationUrl |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| destinationUrl | string | new https landing page |
| supplementaryText | string | up to 100 characters |
| thumbnailAssetIds | array | up to 10; an empty list clears them |
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.
| Argument | Type | Description |
|---|---|---|
| postId* | string | the post id (t3_…) |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| allowComments* | boolean | REQUIRED — Reddit demands allow_comments on every post update, and it is the only field it permits |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"postId",
"allowComments"
],
"properties": {
"postId": {
"type": "string",
"description": "the post id (t3_…)"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"allowComments": {
"type": "boolean",
"description": "REQUIRED — Reddit demands allow_comments on every post update, and it is the only field it permits"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| 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 |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| savedAudienceId* | string | id of the saved audience to edit |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"savedAudienceId"
],
"properties": {
"name": {
"type": "string",
"description": "new saved audience name"
},
"targeting": {
"type": "object",
"description": "the targeting keys to change: each key sent replaces that whole list, keys left out are kept, and an empty list clears one",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"savedAudienceId": {
"type": "string",
"description": "id of the saved audience to edit"
}
}
}Set the OUTREACH STATUS and/or a NOTE on a creator already saved in the swipefile. Status is one of new | contacted | replied | booked | passed. The note is free text (deal terms, rate, what was sent). Reads back the updated row. Use list_swipefile to find the key. Free.
| Argument | Type | Description |
|---|---|---|
| key* | string | the saved row's key from list_swipefile, e.g. tiktok:handle |
| note | string | replaces the existing note; pass "" to clear it |
| brand | string | profile id/name from list_brands, this call only |
| status | string | outreach status: new, contacted, replied, booked or passed |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "the saved row's key from list_swipefile, e.g. tiktok:handle"
},
"note": {
"type": "string",
"maxLength": 2000,
"description": "replaces the existing note; pass \"\" to clear it"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"status": {
"enum": [
"new",
"contacted",
"replied",
"booked",
"passed"
],
"type": "string",
"description": "outreach status: new, contacted, replied, booked or passed"
}
}
}Change this account's app settings. language = the language EVERY ad, script and plan is written in from now on (chat replies follow the language the user writes in) (say the language in plain English, e.g. "German", "Japanese", "Brazilian Portuguese") — it applies to renders made over MCP as well as in the app. theme = the app's appearance, "dark" or "light". watchEmail = the weekly competitor-watch email on/off. Only pass what you are changing. Account-wide (every profile), and it takes effect on the next call.
| Argument | Type | Description |
|---|---|---|
| theme | string | app appearance |
| language | string | language for generated ads and copy (chat replies follow the language the user writes in) — e.g. "English", "German", "Japanese" |
| watchEmail | boolean | weekly competitor-watch email on/off |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"theme": {
"enum": [
"dark",
"light"
],
"type": "string",
"description": "app appearance"
},
"language": {
"type": "string",
"description": "language for generated ads and copy (chat replies follow the language the user writes in) — e.g. \"English\", \"German\", \"Japanese\""
},
"watchEmail": {
"type": "boolean",
"description": "weekly competitor-watch email on/off"
}
}
}CORRECT cells in a Google Sheet — write values to an exact range, overwriting whatever is there. This is the fix append_to_sheet cannot make: appending only ever adds rows at the bottom, so without this a wrong number stays wrong forever and the only "correction" is a second row contradicting the first. Pass `range` (e.g. "B2:C5", or "Q3 Report!B2" to name a tab — list_sheet_tabs gives the names) and `values` as an array of row arrays; an anchor cell like "B2" is fine and the block is written down and right from it. Writing into EMPTY cells goes straight through. Writing OVER cells that already hold values is REFUSED first, naming exactly how many filled cells would be overwritten — show the user that, get a yes, then call again with confirm:true. The result is READ BACK from the sheet, so what you report is what the sheet now holds rather than what Google accepted.
| Argument | Type | Description |
|---|---|---|
| range | string | A1 range or anchor cell, e.g. "B2:C5", "B2", or "Q3 Report!B2" (default A1) |
| values | array | array of row arrays to write |
| confirm | boolean | required only when the target range already holds values |
| updates | array | write SEVERAL disjoint ranges in one call, instead of range+values |
| sheetUrl | string | Google Sheets link, instead of spreadsheetId |
| spreadsheetId | string |
Add people to, or remove them from, a Snapchat FIRST_PARTY customer-list audience. Pass plain emails, phone numbers WITH their country code (+1 555 …), or mobile advertising ids — Hermoso normalises them the way Snapchat specifies and SHA-256 hashes them before anything leaves (a 64-hex value is taken as already hashed). ONE schema per call (Snapchat's rule). Values that are not valid are refused and counted, never sent. action 'remove' and 'remove_all' (empties the list) need confirm:true. Matching is Snapchat's: the reply reports what it accepted and the audience's size and targetable status read back. Free.
| Argument | Type | Description |
|---|---|---|
| action | string | default add |
| schema | string | default EMAIL |
| confirm | boolean | REQUIRED true for remove and remove_all |
| audienceId* | string | FIRST_PARTY customer-list audience id from list_snapchat_ads_audiences |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| identifiers | array | plain values or 64-hex SHA-256 hashes; up to 100,000 per request are batched |
Edit a live or paused Snapchat object IN PLACE, only where Snapchat allows it. campaign / ad: name, startTime, endTime. adsquad: name, startTime, endTime, bid, bidStrategy, pixelId, audiences / excludeAudiences (changing targeting sends its ads back to review and briefly pauses a live squad). creative: name, headline (max 34), brandName (max 32), callToAction, landingPageUrl, utm / addUtm — the URL only on WEB_VIEW creatives; a creative edit re-submits every ad using it for review. Refused BY NAME because Snapchat does not allow them: an ad's creative, a squad's optimization goal or billing event, a creative's type. Status and budget are set_snapchat_ads_status and set_snapchat_ads_budget. confirm:true required — without it NOTHING changes and you get each field's before -> after to show the user. The answer is the READ-BACK, per field.
| Argument | Type | Description |
|---|---|---|
| id* | string | the object id (a creative id comes from the ad row’s creative_id or upload_snapchat_ads_creative) |
| bid | number | ad squad bid in the ad account's currency (converted to micro here) |
| utm | object | UTM parameters set on the landing page URL; Snapchat macros like {{campaign.name}} stay literal |
| name | string | new name for the object |
| level* | string | object to edit: campaign, adsquad, ad or creative |
| addUtm |
Change a Stripe payment link (plink_…). active:false DEACTIVATES it — its URL then shows a "no longer available" page, in every ad that points at it — and ALWAYS needs confirm:true, test keys included; active:true reactivates it. allowPromotionCodes shows or hides the promotion-code box; afterCompletionUrl (redirect) or afterCompletionMessage (Stripe's confirmation page); inactiveMessage (up to 500 characters, shown while it is off); metadata. Read back. LIVE key: confirm:true after showing the preview. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| active | boolean | false deactivates the link; true reactivates it |
| confirm | boolean | true to deactivate the link, and to confirm a live-key write |
| metadata | object | key/value metadata to set on the link |
| paymentLinkId* | string | payment link id, plink_..., from list_stripe_payment_links |
| inactiveMessage | string | message shown while the link is off, up to 500 characters |
| afterCompletionUrl | string | URL the buyer is redirected to after paying |
| allowPromotionCodes |
Switch a Stripe promotion code OFF (active:false) or back on (active:true), or change its metadata. Pass promotionCodeId (promo_…) or the code itself. Switching a code off ALWAYS needs confirm:true, test keys included, because anyone holding it from an ad is then refused at checkout. Read back; a code Stripe will not reactivate (its coupon is no longer valid) is reported, not claimed. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).
| Argument | Type | Description |
|---|---|---|
| code | string | the customer-typable code text, if promotionCodeId is not given |
| active | boolean | false switches the code off; true switches it back on |
| confirm | boolean | true to switch a code off, as well as to confirm a live-key write |
| metadata | object | key/value metadata to set on the code |
| promotionCodeId | string | promotion code id, promo_...; give this or code |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"code": {
"type": "string",
"description": "the customer-typable code text, if promotionCodeId is not given"
},
"active": {
"type": "boolean",
"description": "false switches the code off; true switches it back on"
},
"confirm": {
"type": "boolean",
"description": "true to switch a code off, as well as to confirm a live-key write"
},
"metadata": {
"type": "object",
"description": "key/value metadata to set on the code",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"promotionCodeId": {
"type": "string",
"description": "promotion code id, promo_...; give this or code"
}
}
}CHANGE AN EXISTING TAG in the draft workspace: rename it, fix a wrong conversion id or label, move it onto a different trigger, or pause it. Like create_tag_manager_tag this lands in a DRAFT and does not reach the website until a human publishes the container. Hermoso reads the tag first and merges your change on top of it, because Tag Manager's update replaces the whole tag and sending only the changed field would silently strip its parameters and triggers. A tag's type cannot be changed: that is deleting one tag and creating another. If somebody edited the tag in Tag Manager after Hermoso read it the change is REFUSED rather than overwriting their edit, and the fix is to read it again and reapply. To stop a tag running, pause it rather than removing its trigger. 0 credits. Needs Google Tag Manager connected.
| Argument | Type | Description |
|---|---|---|
| name | string | new tag name |
| notes | string | replaces the internal note on the tag |
| tagId* | string | the tagId from list_tag_manager_tags |
| paused | boolean | pause the tag rather than removing its trigger, which is how you stop one running |
| parameter | array | replaces the tag parameters: [{type, key, value}] |
| workspace | string | older name for gtmWorkspace (MCP only) |
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.
| Argument | Type | Description |
|---|---|---|
| adId* | 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 |
| pageId | string | Instant Form id for a lead ad (checked first) |
| confirm | boolean | required when the ad is switched on |
| musicId | string | carousel sound, from list_tiktok_ads_music |
Rename a TikTok custom audience. THAT IS ALL IT CHANGES, and the limit is TikTok’s rather than ours: their only other update path replaces the uploaded customer FILE, which Hermoso deliberately does not build (hashed personal data under TikTok’s own Terms, over a transport this integration does not have, where a wrong hash yields a silently EMPTY audience). Changing WHO is in an audience means creating a new one. The new name is READ BACK from TikTok because their update returns an empty body, so a bare success proves nothing — if the read-back disagrees with what you sent, the reply says so and the read-back is the truth.
| Argument | Type | Description |
|---|---|---|
| name* | string | the new name, ≤128 characters |
| audienceId* | string | from list_tiktok_ads_audiences |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the new name, ≤128 characters"
},
"audienceId": {
"type": "string",
"description": "from list_tiktok_ads_audiences"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}Replace an automated rule’s WHOLE definition. TikTok’s update is a full replace rather than a patch, so send every field the rule should keep — anything omitted is gone. IF THE RULE IS CURRENTLY RUNNING AND THE NEW VERSION CAN START OR RAISE SPEND, this arms it the moment it saves, so it takes the same confirm:true plus confirmScope echo as arming one: an edit must never be a route by which a harmless rule becomes a spending rule behind a gate it passed when it meant something else. A rule that is currently OFF needs no gate here, because arming it later runs the gate against the new definition. Free.
| Argument | Type | Description |
|---|---|---|
| name* | string | rule name; the update replaces the whole rule, so send the name again |
| tzone | string | timezone for the rule's time windows, sent to TikTok as given |
| ruleId* | string | from list_tiktok_ads_rules |
| actions* | array | what the rule does on a match; replaces the existing actions |
| confirm | boolean | required when the rule is live and the new version can spend |
| conditions* | array | when the rule fires; replaces the existing conditions |
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.
| Argument | Type | Description |
|---|---|---|
| campaignId* | string | TikTok One campaign id from list_tiktok_tto_campaigns |
| handleNames | array | creators to ADD: max 100, CAMPAIGN only |
| campaignType | string | default CAMPAIGN |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
| advertiserIds | array | ad accounts to ADD: max 50 |
| sendNotification | boolean | default false |
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.
| Argument | Type | Description |
|---|---|---|
| name | string | new audience name |
| users | array | one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId} |
| emails | array | plain addresses or 64-hex SHA-256 hashes |
| phones | array | E.164, e.g. +14155550100, or hashes |
| handles | array | X handles, with or without @ |
| accountId | string | older name for adAccountId (MCP only) |
| deviceIds | array | IDFA / AdID / Android id |
| expiresAt | string | ISO 8601 time the members expire; must be later than effectiveAt |
| operation | string |
Change a LIVE X campaign — budget, name, delivery pacing. THIS IS HOW YOU THROTTLE OR RAISE SPEND on a running campaign without rebuilding it, and lowering dailyBudget is the fastest way to slow money down short of pausing. Budgets are in the ad account's own currency. Only fields X actually applies are offered: startTime/endTime are DEPRECATED on an X campaign (its schedule lives on the LINE ITEM — use update_x_ads_line_item) and frequency capping needs an X account feature we cannot enable, so both are refused BY NAME with the reason instead of being sent and silently ignored. THE READ-BACK IS A DIFF against the before-state: a field X did not move is reported as REFUSED, never counted as applied. Print the returned note verbatim.
| Argument | Type | Description |
|---|---|---|
| name | string | new campaign name |
| accountId | string | older name for adAccountId (MCP only) |
| campaignId* | string | from list_x_ads_campaigns |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| dailyBudget | number | in the ad account's currency |
| totalBudget | number | new lifetime budget in ad account currency |
| standardDelivery |
Change an X lead form. Only the fields you pass change — but X replaces the fields list and each section (intro, thank-you page, privacy policy) WHOLE, so pass the complete new value of any you touch. An archived form cannot be changed. Read back from X. Free.
| Argument | Type | Description |
|---|---|---|
| name | string | new internal name of the lead form |
| fields | array | replaces the whole field list; each entry as in create_x_ads_lead_form |
| purpose | string | what the leads are used for, shown to the person |
| headline | string | intro card headline |
| accountId | string | older name for adAccountId (MCP only) |
| leadFormId* | string | lead form id from list_x_ads_lead_forms |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| companyName | string | company name shown on the form |
| description |
Change an X line item — BID, bid strategy, SCHEDULE, goal or name. An X campaign's start and end times are deprecated, so the schedule genuinely lives HERE. Lowering `bidAmount` is also the fix when X refuses a campaign budget with “Bid is too close to Budget”. NOT changeable after creation: `objective` and `productType` — X answers 200 and silently keeps the old value (measured), so both are refused BY NAME here and a different objective means a NEW line item. Changing `goal` also requires `bidAmount` (X refuses the goal alone) and that is stated up front rather than relayed. A line-item dailyBudget/totalBudget is only legal when the parent campaign is NOT budget-optimised — that is the campaign's setting, so those two are forwarded and X's own refusal names the remedy rather than Hermoso blocking a legal edit. THE READ-BACK IS A DIFF against an independent re-read: a field X accepted but did not move is reported as REFUSED. Print the returned note verbatim.
| Argument | Type | Description |
|---|---|---|
| goal | string | requires bidAmount too |
| name | string | new line item (ad group) name |
| endTime | string | ISO 8601 end time for this line item |
| accountId | string | older name for adAccountId (MCP only) |
| bidAmount | number | in the ad account's currency |
| startTime | string |
Change the SUBSCRIPTION plan (admin only). No argument lists the monthly plans. Basic ($9.99) is the posting plan: unlimited posting and Autopilot with NO monthly credits (it includes the same 250+ free credits as the Free plan, monthly credits start on Starter, and Autopilot spends credits). Starter, Pro and Studio include monthly credits and no markup on models. Every paid plan posts without a limit (Free: 10 posts a month on 3 channels). New subscriber: a Checkout URL for the human. Existing subscriber upgrading: a quote + quote_token; once the human agrees, call again with confirm:true and quote_token to charge the card on file. Downgrade: a billing-page link; it starts at the term's end.
| Argument | Type | Description |
|---|---|---|
| plan | string | the plan id to move to (e.g. pro) — omit to list the available plans first |
| period | string | monthly only ('yr' buys monthly) |
| confirm | boolean | true (with quote_token) to make a quoted upgrade |
| quote_token | string | the quoteToken from the quote step |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"plan": {
"type": "string",
"description": "the plan id to move to (e.g. pro) — omit to list the available plans first"
},
"period": {
"enum": [
"mo",
"yr"
],
"type": "string",
"description": "monthly only ('yr' buys monthly)"
},
"confirm": {
"type": "boolean",
"description": "true (with quote_token) to make a quoted upgrade"
},
"quote_token": {
"type": "string",
"description": "the quoteToken from the quote step"
}
}
}Upload an image into the Apple Ads asset library for an APPLE MAPS brand creative. APPLE MAPS ONLY, and that is Apple’s rule rather than ours: it refuses uploads for App Store apps outright, because an App Store ad is rendered from the app’s product page in App Store Connect and not from an image anyone uploads. That refusal is stated up front instead of being sent and rejected, so if you want a different-looking App Store ad the answer is App Store Connect, not this tool. PNG, JPG or HEIC. Pass imageUrl (upload_file turns a local file into one) and mapsBrandId, which list_apple_ads_brands returns. Apple processes the upload asynchronously, so re-read it with list_apple_ads_assets until its eligibility says it is ready before referencing it in a creative. Uploading spends nothing. Free.
| Argument | Type | Description |
|---|---|---|
| brandId | string | older name for mapsBrandId (MCP only) |
| filename | string | Decides the format Apple is told; defaults to the name in the URL. |
| imageUrl* | string | REQUIRED — a PNG, JPG or HEIC image Hermoso can fetch. |
| mapsBrandId | string | REQUIRED: the Apple Maps brand this asset belongs to. |
| promotedObjectType | string | Only BUSINESS_BRAND is accepted. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"imageUrl"
],
"properties": {
"brandId": {
"type": "string",
"description": "older name for mapsBrandId (MCP only)"
},
"filename": {
"type": "string",
"description": "Decides the format Apple is told; defaults to the name in the URL."
},
"imageUrl": {
"type": "string",
"description": "REQUIRED — a PNG, JPG or HEIC image Hermoso can fetch."
},
"mapsBrandId": {
"type": "string",
"description": "REQUIRED: the Apple Maps brand this asset belongs to."
},
"promotedObjectType": {
"enum": [
"BUSINESS_BRAND"
],
"type": "string",
"description": "Only BUSINESS_BRAND is accepted."
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| urls | array | files to upload; a local file goes through upload_file first |
| uploadId | string | re-check an earlier upload instead |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "files to upload; a local file goes through upload_file first"
},
"uploadId": {
"type": "string",
"description": "re-check an earlier upload instead"
}
}
}Persist an ARBITRARY user file (image, video, audio, PDF or document, up to 150MB) into Hermoso and get back a durable public URL that EVERY publish, schedule and ad-build tool accepts. Kept in the Library. THIS IS THE BRING-YOUR-OWN-CREATIVE PATH: it is for files that have NOTHING to do with a Hermoso render (media on the user's desktop, an agency's finished ad, a photo they shot), and it means you can publish, schedule and run ads through Hermoso without generating anything here. Provide exactly ONE source — passing two is an error, never a silent preference: `url` (ANY public http(s) link — Hermoso fetches it server-side, so nothing crosses this connection and there is no practical size limit; THIS IS THE ONE THAT ALWAYS WORKS, including on the hosted connector), `dataUri` (a base64 data: URI — keep it under ~15MB, since the bytes travel over this connection). If the file is already at a public https URL, the Meta, Reddit and ChatGPT-Ads tools take it directly and re-host it safely — but LinkedIn (posts and ad creatives), Pinterest, the YouTube thumbnail and upload_google_ads_asset upload the BYTES themselves and therefore refuse an external host, so run it through here first and pass the URL this returns. When in doubt, use this: its URL works everywhere. (Calling the underlying HTTP route directly? POST /api/upload takes the file's RAW BYTES as the request body with its own content-type — NOT multipart/form-data — or ?url=<link> with no body.) Returns {url, kind, bytes}.
| Argument | Type | Description |
|---|---|---|
| url | string | a PUBLIC http(s) URL Hermoso fetches server-side (private/internal addresses are refused, and every redirect hop is re-checked). Works on every surface including the hosted connector, and the bytes never cross this connection — prefer this whenever the file is reachable on the web. |
| name | string | original file name — helps pick the right extension |
| dataUri | string | base64 data: URI of the file bytes (data:<mime>;base64,<…>) — bytes travel over this connection, so keep it small |
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, 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).
| Argument | Type | Description |
|---|---|---|
| name | string | asset name |
| appAdId | string | also add the playable to this App ad (App campaigns for installs only) |
| 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. |
| customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| 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 |
| youtubeVideoId |
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.
| Argument | Type | Description |
|---|---|---|
| title | string | required, ≤100 chars |
| video | string | Hermoso render id, Library id, attachment://N or Hermoso video link |
| action | string | default upload |
| dryRun | boolean | true = build and return the request without uploading any bytes |
| confirm | boolean | true to remove the upload (action remove); every ad using the video stops showing it |
| privacy | string | UNLISTED (default, the only option on Google's channel) or PUBLIC (own channel only, with channelId) |
| uploadId | string | upload id from the upload reply or action list; required for status and remove |
Upload creative(s) — a finished Hermoso ad OR arbitrary user files (e.g. A folder of media from the user’s desktop) — into a connected ad account’s ASSET LIBRARY so the user or a later ad-build step can use them in their OWN campaigns. Pass `url` for one file, or `urls` (up to 20) to BULK-upload in a single call. Each accepts a public https URL, a data: URI, or a Hermoso /generated path; for LOCAL files call upload_file first and pass the url(s) it returns. Image -> image hash; video -> video id.
| Argument | Type | Description |
|---|---|---|
| url | string | a single public https URL / data: URI / /generated path; a URL, not a local file path; use upload_file for local files |
| kind | string | inferred from the URL if omitted |
| name | string | a label for the asset |
| urls | array | up to 20 media URLs/paths for a one-call BULK upload |
| adAccountId* | string | Ad account id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"url": {
"type": "string",
"description": "a single public https URL / data: URI / /generated path; a URL, not a local file path; use upload_file for local files"
},
"kind": {
"enum": [
"image",
"video"
],
"type": "string",
"description": "inferred from the URL if omitted"
},
"name": {
"type": "string",
"description": "a label for the asset"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 20 media URLs/paths for a one-call BULK upload"
},
"adAccountId": {
"type": "string",
"description": "Ad account id"
}
}
}Upload a PLAYABLE ad (an interactive HTML game the advertiser already has) to a Meta ad account. Pass url: ONE HTML file up to 2 MB, or a zip up to 5 MB (under 100 files) with index.html at the top; it must call FbPlayableAd.onCTAClick() and load nothing from outside. Hermoso checks those rules first and refuses a file Meta would reject. Returns playableAssetId: build the ad with create_meta_ad(playableAssetId, adSetId of an App Installs ad set, ONE lead-in video). Meta has no way to delete a playable once uploaded. Free.
| Argument | Type | Description |
|---|---|---|
| url* | string | public https link to the HTML file or zip; upload_file turns a local file into one |
| name | string | display name for the playable in Meta; optional |
| adAccountId* | string | ad account id |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId",
"url"
],
"properties": {
"url": {
"type": "string",
"description": "public https link to the HTML file or zip; upload_file turns a local file into one"
},
"name": {
"type": "string",
"description": "display name for the playable in Meta; optional"
},
"adAccountId": {
"type": "string",
"description": "ad account id"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| urls | array | direct-download https URLs of videos to upload; clips under 6 seconds are refused |
| videoIds | array | existing video ids to check the status of, instead of uploading |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| description | string | description stored with the uploaded video |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "direct-download https URLs of videos to upload; clips under 6 seconds are refused"
},
"videoIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "existing video ids to check the status of, instead of uploading"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"description": {
"type": "string",
"description": "description stored with the uploaded video"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| assets* | array | creatives to store, 1-20 per call; one item per IMAGE, VIDEO, HEADLINE or CTA |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"redditProfileId",
"assets"
],
"properties": {
"assets": {
"type": "array",
"items": {
"type": "object",
"required": [
"type"
],
"properties": {
"url": {
"type": "string",
"description": "IMAGE/VIDEO: a public https url Reddit can download (upload_file gives you one)"
},
"name": {
"type": "string",
"description": "IMAGE/VIDEO: library name; defaults to the file name"
},
"text": {
"type": "string",
"description": "HEADLINE: the headline text"
},
"type": {
"enum": [
"IMAGE",
"VIDEO",
"HEADLINE",
"CTA"
],
"type": "string",
"description": "IMAGE, VIDEO, HEADLINE or CTA; decides which other fields apply"
},
"posterUrl": {
"type": "string",
"description": "VIDEO only: an https url for the poster frame"
},
"referenceId": {
"type": "string",
"description": "your own id, echoed back per upload"
},
"callToAction": {
"type": "string",
"description": "CTA: one of Reddit’s labels — Apply Now | Contact Us | Download | Get a Quote | Get Showtimes | Install | Learn More | Order Now | Play Now | Pre-order Now | See Menu | Shop Now | Sign Up | View More | Watch Now | Book Now | Buy Tickets | Get Directions | Listen Now | Read More | Subscribe | Visit Store | Donate Now | Remind Me"
}
}
},
"maxItems": 20,
"minItems": 1,
"description": "creatives to store, 1-20 per call; one item per IMAGE, VIDEO, HEADLINE or CTA"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_…) — omit when only one is shared"
},
"redditProfileId": {
"type": "string",
"description": "the Reddit profile id (t2_…) from list_reddit_ads_profiles — assets hang off a profile, and it must be on the shared ad account"
}
}
}PUT A FINISHED HERMOSO RENDER ONTO THE SNAPCHAT AD ACCOUNT so an ad can point at it — create_snapchat_ads_ad needs the creativeId it returns. Pass the public https url of a render; SNAPCHAT HAS NO UPLOAD-FROM-URL, so Hermoso fetches the bytes and posts them as multipart. This does TWO things in one call — it uploads the MEDIA and then builds the CREATIVE that wraps it — because a Snapchat ad points at a CREATIVE and never at a media id, and stopping after the upload leaves an asset nothing can use. A PUBLIC PROFILE IS MANDATORY: Snapchat has required profile_properties on every ad creative since 2024-02-26, so snapProfileId is REQUIRED and a call without one is refused BEFORE any bytes move (which is what stops an orphan media row being left on the ad account for a creative that could never be created). Get one from list_snapchat_ads_profiles, or read it in Snapchat Ads Manager. headline is required too. COPY LIMITS ARE SHORT AND ENFORCED: headline max 34 characters, brandName max 32, and over-long copy is REFUSED rather than truncated, because shipping words nobody wrote is worse than a refusal. To build a SECOND creative on media already uploaded, pass mediaId instead of url — nothing is downloaded or uploaded again. A file over 32MB is refused by name (Snapchat requires a chunked upload flow above that which Hermoso does not implement). Free.
| Argument | Type | Description |
|---|---|---|
| url | string | public https url of the render — a Hermoso render URL already is one; for any other file run it through upload_file first. Omit only when reusing mediaId. |
| utm | object | UTM parameters appended to landingPageUrl. Snapchat URL macros such as {{campaign.name}}, {{adSet.name}}, {{ad.id}} are kept literal so Snapchat fills them in |
| kind | string | default video |
| name | string |
Put a finished creative onto the TikTok AD ACCOUNT so an ad can point at it — this is the bridge between making an ad and running one, and create_tiktok_ads_ad has no other source for the ids it needs. Pass the public https url of a Hermoso render (every render already is one) or any other public https file; TikTok fetches it from its own servers, so a local path, a data: url or anything on http:// is refused here for free. A VIDEO UPLOAD ALSO UPLOADS ITS OWN COVER and hands back coverImageId — pass that straight to create_tiktok_ads_ad as imageIds, because TikTok requires a cover on a video ad and rejects any whose dimensions differ from the video. CHECK usableInAds BEFORE BUILDING THE AD: TikTok happily accepts a creative it will then refuse to build an ad from, and says so only in this flag — a 720x1280 render is fine and a 496x864 one is not, so a false here means re-render larger, and the note says so. Uploading the same file twice is fine; TikTok refuses a repeated file NAME, so leave fileName out unless you want a specific label in TikTok Ads Manager. Free — TikTok charges nothing for storage and Hermoso bills no credits for this.
| Argument | Type | Description |
|---|---|---|
| url* | string | public https url of the file. A Hermoso render url works as-is; for a local or external file run upload_file first to get one. |
| kind | string | default video |
| fileName | string | the label in TikTok Ads Manager. Omit it and a unique one is derived — TikTok rejects a DUPLICATE name on the same advertiser, so an explicit name that has been used before is refused. |
| advertiserId | string | from list_tiktok_ads_accounts — omit only when exactly one is reachable |
Upload a PLAYABLE ad (an interactive HTML game the advertiser already has) into a TikTok advertiser's Creative Library. Pass url: a zip with index.html and config.json ({"playable_orientation": 0|1|2}) at the top level. Hermoso checks it against TikTok's rules first (no MRAID, calls window.openAppStore(), loads TikTok's playable SDK, no other outside files, under 5 MB) and refuses a file TikTok would reject, then TikTok validates it and it is saved to the library. The reply carries playableUrl: pass it to create_tiktok_smart_ad as playableUrls. TikTok runs playables only in App Promotion Smart+ ads with a video, for advertisers it has allowlisted. Pass playableId instead of url to finish one TikTok was still checking. Free.
| Argument | Type | Description |
|---|---|---|
| url | string | public https link to the zip; upload_file turns a local file into one |
| name | string | name in the Creative Library, 20 characters max (TikTok's limit) |
| playableId | string | finish an upload TikTok was still validating |
| advertiserId | string | from list_tiktok_ads_accounts; omit when one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "public https link to the zip; upload_file turns a local file into one"
},
"name": {
"type": "string",
"description": "name in the Creative Library, 20 characters max (TikTok's limit)"
},
"playableId": {
"type": "string",
"description": "finish an upload TikTok was still validating"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts; omit when one is shared"
}
}
}Upload an image or video (any URL — a Hermoso render, an upload_file URL, a public file) to an X ad account and return its media_key, for create_x_ads_card or create_x_ads_post. Added to the ad account's Media Library and read back. A card image must be 1:1 ≥600×600, 1.91:1 ≥600×314, 16:9 ≥600×337, 9:16 ≥600×1066, 2:3 ≥600×900 or 4:5 ≥600×750. Free.
| Argument | Type | Description |
|---|---|---|
| url* | string | public image or video URL: a Hermoso render, an upload_file URL or any public file |
| name | string | label in the Media Library |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "public image or video URL: a Hermoso render, an upload_file URL or any public file"
},
"name": {
"type": "string",
"description": "label in the Media Library"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| mpn | string | manufacturer part number |
| gtin | string | GTIN barcode (UPC, EAN or similar) |
| link | string | the product page URL on the merchant's store |
| size | string | product size |
| brand | string | brand name |
| color |
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.
| Argument | Type | Description |
|---|---|---|
| dryRun | boolean | validate against Microsoft and write nothing — Microsoft returns no ids for a dry run, so none is claimed |
| 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 | array | several whole offers in one batch, maximum 300 |
| accountId | string | older name for adAccountId (MCP only) |
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.
| Argument | Type | Description |
|---|---|---|
| products* | array | each {id, title, price:"19.99 USD", link, image_link, availability, …} |
| catalogId* | string | catalog to add or update products in |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId",
"products"
],
"properties": {
"products": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "each {id, title, price:\"19.99 USD\", link, image_link, availability, …}"
},
"catalogId": {
"type": "string",
"description": "catalog to add or update products in"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}Pin which profile this connection acts on. Pass the profile id or exact name from list_brands. Works for your own profiles AND one another account SHARED with you (pass its name or the profile id list_brands prints); access is verified against your real membership before it is pinned. Persists for this API key until changed, on every surface (hosted connector included — no environment variables, no restart).
| Argument | Type | Description |
|---|---|---|
| brand* | string | profile id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of one shared with you |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"brand"
],
"properties": {
"brand": {
"type": "string",
"description": "profile id (e.g. default / p_xxx), its exact name from list_brands, or the profile id of one shared with you"
}
}
}Vercel CUSTOM EVENTS, the CONVERSIONS on a Vercel-hosted site (signups, purchases, any track('Name') from @vercel/analytics): the count and unique visitors over a window, and with `by` a breakdown by eventName, eventData/<property> (e.g. eventData/plan), utmSource / utmMedium / utmCampaign / utmContent / utmTerm (which ad or campaign drove the conversion), requestPath, country and the rest, or a time step. Up to TWO at once, e.g. by 'utmCampaign,eventName' or 'day,utmCampaign'. eventName narrows to one event. This is the conversion half of an ad report for a landing page on Vercel; vercel_web_analytics is the traffic half. Custom events need Web Analytics on and a Vercel Pro or Enterprise plan. Read-only, free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Argument | Type | Description |
|---|---|---|
| by | string | one or TWO comma-separated dimensions: eventName, eventData, eventData/<property>, utmSource, utmMedium, utmCampaign, utmContent, utmTerm, requestPath, referrerHostname, country, deviceType, osName, browserName, route, environment, flags, flags/<key>, or one time step hour/day/week/month/year |
| days | number | 1-366, default 30 |
| limit | number | rows in the breakdown, 1-100, default 10 |
| since | string | YYYY-MM-DD (instead of days) |
| until | string | YYYY-MM-DD (default now) |
| filter |
Vercel WEB ANALYTICS for one project: page views and unique visitors over a window (default the last 30 days, production traffic), and with `by` a breakdown by requestPath (which pages), referrerHostname (where visitors came from), country, deviceType, osName, browserName, route, utmSource / utmMedium / utmCampaign / utmContent / utmTerm (which campaigns and ads brought them), or a time step (day, week, month). Use it to see which landing pages and ad campaigns actually bring traffic. Needs Web Analytics turned on for the project in Vercel. Read-only, free. Needs Vercel connected (Settings > Connectors > Vercel, or connect_connector with an access token).
| Argument | Type | Description |
|---|---|---|
| by | string | one or TWO comma-separated dimensions (at most one time step): requestPath, referrerHostname, country, deviceType, osName, browserName, route, utmSource, utmMedium, utmCampaign, utmContent, utmTerm, environment, flags, flags/<key>, hour, day, week, month, year — e.g. 'day,utmCampaign' |
| days | number | 1-366, default 30 |
| limit | number | rows in the breakdown, 1-100, default 10 |
| since | string | YYYY-MM-DD (instead of days) |
| until | string | YYYY-MM-DD (default now) |
| filter | string | Vercel's OData filter, e.g. utmSource eq 'facebook' |
Ask Bing to CHECK the ownership proof for a site already on the account. IT DOES NOT PLACE THE PROOF — nothing can over an API. The user puts an XML file at the site root, a meta tag in the home page <head>, or a CNAME DNS record, and Bing Webmaster Tools shows the exact filename and value for each. A negative answer therefore means "the proof is not in place yet", never that Hermoso or the connection failed, and the reply says which three options exist. Bing refuses to verify a site that was never added, which is knowable for free, so that is refused here with add_bing_webmaster_site named as the fix. The verdict is read back from the site LIST rather than taken from the call's own return value — the two can disagree. 0 credits.
| Argument | Type | Description |
|---|---|---|
| siteUrl* | string | the site to verify, exactly as it was added |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl"
],
"properties": {
"siteUrl": {
"type": "string",
"description": "the site to verify, exactly as it was added"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| limit | number | max results to return |
| query* | string | a place name, e.g. "Canada" or "Austin" |
| locationType | string | COUNTRIES, REGIONS, METROS, CITIES or POSTAL_CODES; omit to search all types |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"query"
],
"properties": {
"limit": {
"type": "number",
"description": "max results to return"
},
"query": {
"type": "string",
"description": "a place name, e.g. \"Canada\" or \"Austin\""
},
"locationType": {
"enum": [
"COUNTRIES",
"REGIONS",
"METROS",
"CITIES",
"POSTAL_CODES"
],
"type": "string",
"description": "COUNTRIES, REGIONS, METROS, CITIES or POSTAL_CODES; omit to search all types"
}
}
}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.
| Argument | Type | Description |
|---|---|---|
| entity | string | default CAMPAIGN |
| endTime | string | ISO 8601; defaults to now |
| accountId | string | older name for adAccountId (MCP only) |
| entityIds* | array | max 20 |
| placement | string | default ALL_ON_TWITTER |
| startTime | string | ISO 8601; defaults to 7 days before endTime |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| granularity | string |
Resolve X targeting ids by name for every vocabulary BEYOND location — INTEREST, CONVERSATION, PLATFORM, DEVICE, LANGUAGE, APP_STORE_CATEGORY, NETWORK_OPERATOR, TV_MARKET, TV_SHOW, EVENT. X's targeting ids are opaque (an interest is a 19-digit number, a device is “96”) and X has NO name-based targeting parameter, so this is the only way to obtain one — without it add_x_ads_targeting accepts vocabularies nobody can supply a value for. Pass a result as criteria:[{targetingType, targetingValue}]. Locations have their own tool: x_ads_geo_search. TV_SHOW requires a `locale`, which comes from kind:"TV_MARKET". Read-only, free.
| Argument | Type | Description |
|---|---|---|
| kind* | string | targeting vocabulary to search, e.g. INTEREST, DEVICE, LANGUAGE or EVENT |
| limit | number | maximum number of results to return |
| query | string | filter by name |
| locale | string | TV_SHOW — required; get one from kind:"TV_MARKET" |
| osType | string | APP_STORE_CATEGORY |
| eventTypes | string | EVENT |
| countryCode | string | NETWORK_OPERATOR, e.g. US |
We read every tool's name, description and inputs the way the model will, looking for instructions hidden from you. A clean result means no warning signs, not a guarantee. Scanned Nov 6, 2026.
Takes a password, API key or token as an argument, so the secret passes through the model's context and chat logs.
Fix: Read credentials from the server's own configuration (environment, OAuth) instead of a tool argument.
| confirm |
| boolean |
| set true ONLY after the user approved changing what a LIVE campaign shows |
| adGroupId | string | ad group id (level:"adGroup") |
| assetType* | string | what kind of asset to create and attach |
| campaignId | string | campaign id (level:"campaign") |
| 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 |
| assetResourceNames | array | attach assets that ALREADY exist instead of creating new ones |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"assetType"
],
"properties": {
"level": {
"enum": [
"campaign",
"adGroup",
"customer"
],
"type": "string",
"description": "where to attach it — follows the id you pass; customer = every campaign on the account"
},
"assets": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "optional asset name in the library"
},
"image": {
"type": "string",
"description": "BUSINESS_LOGO — render id, Library id, attachment://N or Hermoso image link"
},
"fields": {
"type": "array",
"items": {
"type": "string"
},
"description": "LEAD_FORM — default name + email"
},
"header": {
"type": "string",
"description": "STRUCTURED_SNIPPET — e.g. \"Services\", \"Brands\", \"Types\""
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "STRUCTURED_SNIPPET — at least 3 values"
},
"endDate": {
"type": "string",
"description": "end date, YYYY-MM-DD"
},
"finalUrl": {
"type": "string",
"description": "SITELINK — the page it opens"
},
"headline": {
"type": "string",
"description": "LEAD_FORM: headline shown on the form"
},
"linkText": {
"type": "string",
"description": "SITELINK — the clickable label, ≤25 characters"
},
"occasion": {
"type": "string",
"description": "PROMOTION — e.g. BLACK_FRIDAY, SUMMER_SALE"
},
"offerings": {
"type": "array",
"items": {
"type": "object",
"required": [
"header",
"description",
"price",
"finalUrl"
],
"properties": {
"unit": {
"type": "string",
"description": "price unit, e.g. per month; optional"
},
"price": {
"type": "number",
"description": "offering price in major units, in currencyCode"
},
"header": {
"type": "string",
"description": "offering name, up to 25 characters"
},
"finalUrl": {
"type": "string",
"description": "page the offering opens"
},
"description": {
"type": "string",
"description": "offering detail, up to 25 characters"
}
}
},
"description": "PRICE: 3 to 8 offerings, each {header, description, price, unit, finalUrl}"
},
"priceType": {
"type": "string",
"description": "PRICE — SERVICES, PRODUCT_CATEGORIES, PRODUCT_TIERS, BRANDS, EVENTS, LOCATIONS…"
},
"startDate": {
"type": "string",
"description": "start date, YYYY-MM-DD"
},
"percentOff": {
"type": "number",
"description": "PROMOTION: percent off, e.g. 20 for 20%; use this or moneyAmountOff, not both"
},
"calloutText": {
"type": "string",
"description": "CALLOUT — ≤25 characters, e.g. \"Free 2-day shipping\""
},
"countryCode": {
"type": "string",
"description": "CALL — two-letter country of the number"
},
"description": {
"type": "string",
"description": "LEAD_FORM: description text shown on the form"
},
"phoneNumber": {
"type": "string",
"description": "CALL: phone number to show, with country code"
},
"businessName": {
"type": "string",
"description": "business name, up to 25 characters; BUSINESS_NAME and LEAD_FORM"
},
"currencyCode": {
"type": "string",
"description": "ISO 4217 currency for money amounts and offering prices, e.g. USD"
},
"description1": {
"type": "string",
"description": "SITELINK — first description line"
},
"description2": {
"type": "string",
"description": "SITELINK — second description line"
},
"languageCode": {
"type": "string",
"description": "language of the asset, e.g. en"
},
"promotionCode": {
"type": "string",
"description": "PROMOTION: coupon code the customer enters"
},
"finalMobileUrl": {
"type": "string",
"description": "SITELINK — a different page on mobile"
},
"moneyAmountOff": {
"type": "number",
"description": "PROMOTION: money off in currencyCode, e.g. 10 for 10 off; use this or percentOff"
},
"priceQualifier": {
"enum": [
"FROM",
"UP_TO",
"AVERAGE"
],
"type": "string",
"description": "PRICE: FROM, UP_TO or AVERAGE"
},
"promotionTarget": {
"type": "string",
"description": "PROMOTION: what the offer is on, up to 20 characters"
},
"callToActionType": {
"type": "string",
"description": "LEAD_FORM: call-to-action button type, e.g. GET_QUOTE"
},
"ordersOverAmount": {
"type": "number",
"description": "PROMOTION: minimum order value that qualifies, in currencyCode"
},
"privacyPolicyUrl": {
"type": "string",
"description": "LEAD_FORM: https URL of the privacy policy"
},
"callToActionDescription": {
"type": "string",
"description": "LEAD_FORM: text shown beside the call-to-action button"
}
}
},
"description": "the assets to CREATE and attach"
},
"dryRun": {
"type": "boolean",
"description": "validate against Google and create NOTHING"
},
"status": {
"enum": [
"ENABLED",
"PAUSED"
],
"type": "string",
"description": "the LINK status — default ENABLED"
},
"confirm": {
"type": "boolean",
"description": "set true ONLY after the user approved changing what a LIVE campaign shows"
},
"adGroupId": {
"type": "string",
"description": "ad group id (level:\"adGroup\")"
},
"assetType": {
"enum": [
"SITELINK",
"CALLOUT",
"STRUCTURED_SNIPPET",
"CALL",
"PROMOTION",
"PRICE",
"LEAD_FORM",
"BUSINESS_NAME",
"BUSINESS_LOGO"
],
"type": "string",
"description": "what kind of asset to create and attach"
},
"campaignId": {
"type": "string",
"description": "campaign id (level:\"campaign\")"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"assetResourceNames": {
"type": "array",
"items": {
"type": "string"
},
"description": "attach assets that ALREADY exist instead of creating new ones"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"keywords"
],
"properties": {
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and create nothing"
},
"confirm": {
"type": "boolean",
"description": "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"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "≤80 characters, ≤10 words"
},
"paused": {
"type": "boolean",
"description": "true = create this keyword paused, so it does not bid"
},
"negative": {
"type": "boolean",
"description": "true = BLOCK this term instead of targeting it"
},
"cpcBidUsd": {
"type": "number",
"description": "per-keyword max CPC"
},
"matchType": {
"enum": [
"EXACT",
"PHRASE",
"BROAD"
],
"type": "string",
"description": "default PHRASE"
}
}
},
"description": "the keywords to add"
},
"adGroupId": {
"type": "string",
"description": "the ad group to add them to"
},
"customerId": {
"type": "string",
"description": "omit to use the brand’s selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"end": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default today)"
},
"start": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default 30 days ago)"
},
"metric": {
"enum": [
"active",
"new"
],
"type": "string",
"description": "default active"
},
"groupBy": {
"type": "string",
"description": "ONE property to group by"
},
"interval": {
"type": "number",
"description": "1 daily, 7 weekly, 30 monthly"
},
"segments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {
"type": "string",
"description": "property operator, e.g. is, is not, contains; symbol (>=) on a who-performed condition"
},
"prop": {
"type": "string",
"description": "user property to filter on: built-in (country, platform), gp:<name> for custom, or userdata_cohort"
},
"type": {
"type": "string",
"description": "'event' for a who-performed condition; omit for a property filter"
},
"value": {
"type": "number",
"description": "who-performed count threshold; default 1 (at least once)"
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "values the property must match, e.g. [\"summer\"]"
},
"time_type": {
"type": "string",
"description": "rolling (the only accepted value; default)"
},
"event_type": {
"type": "string",
"description": "event name for a who-performed condition"
},
"time_value": {
"type": "number",
"description": "who-performed look-back in days; default 30"
}
},
"additionalProperties": {}
},
"description": "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."
}
}
}| array |
| 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. |
| conversionWindowSeconds | number | Amplitude default is 2592000 (30 days) — say which window you used |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"events"
],
"properties": {
"end": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default today)"
},
"mode": {
"enum": [
"ordered",
"unordered",
"sequential"
],
"type": "string",
"description": "default ordered"
},
"start": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default 30 days ago)"
},
"users": {
"enum": [
"active",
"new"
],
"type": "string",
"description": "'new' = first-time users only (the acquisition funnel); Amplitude's default is active"
},
"events": {
"type": "array",
"items": {},
"description": "ordered steps — plain event names, or {eventType, filters:[…]} objects"
},
"groupBy": {
"type": "string",
"description": "at most ONE — a second is refused"
},
"segments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {
"type": "string",
"description": "property operator, e.g. is, is not, contains; symbol (>=) on a who-performed condition"
},
"prop": {
"type": "string",
"description": "user property to filter on: built-in (country, platform), gp:<name> for custom, or userdata_cohort"
},
"type": {
"type": "string",
"description": "'event' for a who-performed condition; omit for a property filter"
},
"value": {
"type": "number",
"description": "who-performed count threshold; default 1 (at least once)"
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "values the property must match, e.g. [\"summer\"]"
},
"time_type": {
"type": "string",
"description": "rolling (the only accepted value; default)"
},
"event_type": {
"type": "string",
"description": "event name for a who-performed condition"
},
"time_value": {
"type": "number",
"description": "who-performed look-back in days; default 30"
}
},
"additionalProperties": {}
},
"description": "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."
},
"conversionWindowSeconds": {
"type": "number",
"description": "Amplitude default is 2592000 (30 days) — say which window you used"
}
}
}| startEvent | string | "_new" (default) or an event name |
| returnEvent | string | "_active" (default) or an event name |
| retentionMode | string | n-day, bracket (needs brackets) or rolling |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"end": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default today)"
},
"start": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default 30 days ago)"
},
"groupBy": {
"type": "string",
"description": "ONE property to group by"
},
"brackets": {
"type": "array",
"items": {
"type": "number"
},
"description": "required when retentionMode is \"bracket\""
},
"interval": {
"type": "number",
"description": "1 daily, 7 weekly, 30 monthly"
},
"segments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {
"type": "string",
"description": "property operator, e.g. is, is not, contains; symbol (>=) on a who-performed condition"
},
"prop": {
"type": "string",
"description": "user property to filter on: built-in (country, platform), gp:<name> for custom, or userdata_cohort"
},
"type": {
"type": "string",
"description": "'event' for a who-performed condition; omit for a property filter"
},
"value": {
"type": "number",
"description": "who-performed count threshold; default 1 (at least once)"
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "values the property must match, e.g. [\"summer\"]"
},
"time_type": {
"type": "string",
"description": "rolling (the only accepted value; default)"
},
"event_type": {
"type": "string",
"description": "event name for a who-performed condition"
},
"time_value": {
"type": "number",
"description": "who-performed look-back in days; default 30"
}
},
"additionalProperties": {}
},
"description": "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."
},
"startEvent": {
"type": "string",
"description": "\"_new\" (default) or an event name"
},
"returnEvent": {
"type": "string",
"description": "\"_active\" (default) or an event name"
},
"retentionMode": {
"enum": [
"n-day",
"bracket",
"rolling"
],
"type": "string",
"description": "n-day, bracket (needs brackets) or rolling"
}
}
}| string |
| required when metric is "formula" |
| 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 |
| interval | number | -300000 realtime, -3600000 hourly, 1 daily, 7 weekly, 30 monthly |
| segments | array | 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"end": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default today)"
},
"event": {
"type": "string",
"description": "shorthand for a single event name"
},
"limit": {
"type": "number",
"description": "≤1000"
},
"start": {
"type": "string",
"description": "YYYYMMDD or YYYY-MM-DD (default 30 days ago)"
},
"events": {
"type": "array",
"items": {},
"description": "one or two event specs: a plain name, or {eventType, filters:[{subprop_type,subprop_key,subprop_op,subprop_value}], groupBy:[…]}"
},
"metric": {
"enum": [
"uniques",
"totals",
"pct_dau",
"average",
"histogram",
"sums",
"value_avg",
"formula"
],
"type": "string",
"description": "default uniques"
},
"formula": {
"type": "string",
"description": "required when metric is \"formula\""
},
"groupBy": {
"type": "string",
"description": "user property or event property to break the series down by, e.g. gp:utm_campaign or country"
},
"groupBy2": {
"type": "string",
"description": "a SECOND group-by (Amplitude's g2), e.g. groupBy gp:utm_campaign + groupBy2 country"
},
"interval": {
"type": "number",
"description": "-300000 realtime, -3600000 hourly, 1 daily, 7 weekly, 30 monthly"
},
"segments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {
"type": "string",
"description": "property operator, e.g. is, is not, contains; symbol (>=) on a who-performed condition"
},
"prop": {
"type": "string",
"description": "user property to filter on: built-in (country, platform), gp:<name> for custom, or userdata_cohort"
},
"type": {
"type": "string",
"description": "'event' for a who-performed condition; omit for a property filter"
},
"value": {
"type": "number",
"description": "who-performed count threshold; default 1 (at least once)"
},
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "values the property must match, e.g. [\"summer\"]"
},
"time_type": {
"type": "string",
"description": "rolling (the only accepted value; default)"
},
"event_type": {
"type": "string",
"description": "event name for a who-performed condition"
},
"time_value": {
"type": "number",
"description": "who-performed look-back in days; default 30"
}
},
"additionalProperties": {}
},
"description": "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."
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"action": {
"enum": [
"get",
"set"
],
"type": "string",
"description": "defaults to 'get'"
},
"confirm": {
"type": "boolean",
"description": "required when the change SHORTENS retention, which deletes data"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id, from list_analytics_properties"
},
"userDataRetention": {
"type": "string",
"description": "months: 2 or 14"
},
"eventDataRetention": {
"type": "string",
"description": "months: 2 or 14 (26/38/50 Analytics 360 only), or the enum e.g. FOURTEEN_MONTHS"
},
"resetUserDataOnNewActivity": {
"type": "boolean",
"description": "true = a user's data retention window restarts each time that user shows new activity"
}
}
}| compare |
| string |
| also read the period before startDate–endDate (or the same dates a year earlier) and return `change` |
| endDate | string | YYYY-MM-DD or "today" (default today) |
| metrics | array | GA4 metric names — default ["activeUsers","sessions","screenPageViews"] |
| orderBy | array | [{dimension:"date"}] or [{metric:"sessions", desc:true}] |
| property* | 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 |
| orderDesc | boolean | default true (largest first) when orderByMetric is set |
| startDate | string | YYYY-MM-DD or a GA4 relative date like "28daysAgo" (default 28daysAgo) |
| dateRanges | array | up to 4 periods instead of startDate/endDate; rows gain a dateRange column; with two, change = the FIRST relative to the SECOND |
| dimensions | array | GA4 dimension names to break the metrics down by — omit for a single total row |
| comparisons | array | segments side by side: [{name, dimensionFilter}] or [{comparison:"comparisons/123"}]; adds a comparison column |
| aggregations | array | extra summary rows over the result: TOTAL, MINIMUM, MAXIMUM or COUNT |
| currencyCode | string | ISO 4217, e.g. EUR — default the property currency |
| 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. |
| keepEmptyRows | boolean | true = keep rows whose metrics are all zero |
| orderByMetric | string | sort by this metric — must be one of the metrics requested |
| 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"limit": {
"type": "number",
"description": "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": {
"type": "number",
"description": "skip this many rows — how you page past `limit`. Use the nextOffset the previous call returns."
},
"totals": {
"type": "boolean",
"description": "add TOTAL rows"
},
"compare": {
"enum": [
"previous_period",
"previous_year"
],
"type": "string",
"description": "also read the period before startDate–endDate (or the same dates a year earlier) and return `change`"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD or \"today\" (default today)"
},
"metrics": {
"type": "array",
"items": {
"type": "string"
},
"description": "GA4 metric names — default [\"activeUsers\",\"sessions\",\"screenPageViews\"]"
},
"orderBy": {
"type": "array",
"items": {
"type": "object",
"properties": {
"desc": {
"type": "boolean",
"description": "true = largest or latest first"
},
"metric": {
"type": "string",
"description": "metric to sort by; use instead of dimension"
},
"dimension": {
"type": "string",
"description": "dimension to sort by; use instead of metric"
},
"orderType": {
"enum": [
"ALPHANUMERIC",
"CASE_INSENSITIVE_ALPHANUMERIC",
"NUMERIC"
],
"type": "string",
"description": "dimension sort order: ALPHANUMERIC, CASE_INSENSITIVE_ALPHANUMERIC or NUMERIC"
}
}
},
"description": "[{dimension:\"date\"}] or [{metric:\"sessions\", desc:true}]"
},
"property": {
"type": "string",
"description": "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"
},
"orderDesc": {
"type": "boolean",
"description": "default true (largest first) when orderByMetric is set"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD or a GA4 relative date like \"28daysAgo\" (default 28daysAgo)"
},
"dateRanges": {
"type": "array",
"items": {
"type": "object",
"required": [
"startDate",
"endDate"
],
"properties": {
"name": {
"type": "string",
"description": "label for this period, shown in the dateRange column"
},
"endDate": {
"type": "string",
"description": "period end, YYYY-MM-DD or today"
},
"startDate": {
"type": "string",
"description": "period start, YYYY-MM-DD or a GA4 relative date such as 28daysAgo"
}
}
},
"maxItems": 4,
"description": "up to 4 periods instead of startDate/endDate; rows gain a dateRange column; with two, change = the FIRST relative to the SECOND"
},
"dimensions": {
"type": "array",
"items": {
"type": "string"
},
"description": "GA4 dimension names to break the metrics down by — omit for a single total row"
},
"comparisons": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "label for this segment; becomes the value of the comparison column"
},
"comparison": {
"type": "string",
"description": "saved GA4 comparison id such as comparisons/1234; use instead of name and dimensionFilter"
},
"dimensionFilter": {
"type": "object",
"description": "FilterExpression selecting this segment, same shape as dimensionFilter",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"description": "segments side by side: [{name, dimensionFilter}] or [{comparison:\"comparisons/123\"}]; adds a comparison column"
},
"aggregations": {
"type": "array",
"items": {
"enum": [
"TOTAL",
"MINIMUM",
"MAXIMUM",
"COUNT"
],
"type": "string"
},
"description": "extra summary rows over the result: TOTAL, MINIMUM, MAXIMUM or COUNT"
},
"currencyCode": {
"type": "string",
"description": "ISO 4217, e.g. EUR — default the property currency"
},
"metricFilter": {
"type": "object",
"description": "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.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"keepEmptyRows": {
"type": "boolean",
"description": "true = keep rows whose metrics are all zero"
},
"orderByMetric": {
"type": "string",
"description": "sort by this metric — must be one of the metrics requested"
},
"dimensionFilter": {
"type": "object",
"description": "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.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}| string |
| what to optimise for, in the user's own words |
| level | string | default campaign |
| platforms | array | meta, google_ads, openai_ads, tiktok_ads, linkedin, microsoft_ads, pinterest_ads, reddit_ads, snapchat_ads, x_ads, apple_ads — omit for every connected ad platform |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"days": {
"type": "number",
"description": "how many complete days back, ending yesterday (default 14, max 90)"
},
"goal": {
"type": "string",
"description": "what to optimise for, in the user's own words"
},
"level": {
"enum": [
"campaign",
"ad_group",
"ad"
],
"type": "string",
"description": "default campaign"
},
"platforms": {
"type": "array",
"items": {
"type": "string"
},
"description": "meta, google_ads, openai_ads, tiktok_ads, linkedin, microsoft_ads, pinterest_ads, reddit_ads, snapchat_ads, x_ads, apple_ads — omit for every connected ad platform"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"end": {
"type": "string",
"description": "YYYY-MM-DD."
},
"limit": {
"type": "number",
"description": "maximum number of changes to return"
},
"start": {
"type": "string",
"description": "YYYY-MM-DD. Required unless end or an explicit eventTime filter is given."
},
"offset": {
"type": "number",
"description": "changes to skip, for paging"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "Extra conditions, e.g. [{\"field\":\"campaignId\",\"operator\":\"EQUALS\",\"value\":\"123\"}]. Non-time fields accept only EQUALS and IN."
},
"detailId": {
"type": "string",
"description": "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."
}
}
}| granularity |
| string |
| Default DAILY (max 30-day window). WEEKLY_SUN_SAT allows at most 4 weeks. |
| countryOrRegion | array | ISO 3166-1 alpha-2, e.g. ["US"]. |
| promotedObjectId* | string | REQUIRED — the app’s adamId. Apple answers 400 without it. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"promotedObjectId",
"start",
"end"
],
"properties": {
"end": {
"type": "string",
"description": "REQUIRED YYYY-MM-DD."
},
"limit": {
"type": "number",
"description": "maximum number of rows to return"
},
"start": {
"type": "string",
"description": "REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT."
},
"offset": {
"type": "number",
"description": "rows to skip, for paging"
},
"reportType": {
"enum": [
"FIRST_SLOT",
"ALL_SLOTS"
],
"type": "string",
"description": "Default FIRST_SLOT."
},
"granularity": {
"enum": [
"DAILY",
"WEEKLY_SUN_SAT"
],
"type": "string",
"description": "Default DAILY (max 30-day window). WEEKLY_SUN_SAT allows at most 4 weeks."
},
"countryOrRegion": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO 3166-1 alpha-2, e.g. [\"US\"]."
},
"promotedObjectId": {
"type": "string",
"description": "REQUIRED — the app’s adamId. Apple answers 400 without it."
}
}
}| offset |
| number |
| suggestions to skip, for paging |
| phrases | array | kind:"phrase" ONLY, and REQUIRED there on the SEARCH route — the phrases to look up, e.g. ["best productivity app"]. |
| queryType | string | phrase/category ONLY (refused on keyword). Default SEARCH. SUGGESTION is measured BROKEN at Apple — ~34s then a server error. |
| categories | array | kind:"category" ONLY, and REQUIRED there on the SEARCH route — the App Store categories to look up, e.g. ["Utilities"]. |
| promotedObjectId* | string | REQUIRED — the app’s adamId, or an Apple Maps brandId. |
| countriesOrRegions | array | ISO 3166-1 alpha-2, e.g. ["US","GB"]. |
| promotedObjectType | string | Default APPSTORE_APP. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"promotedObjectId"
],
"properties": {
"kind": {
"enum": [
"keyword",
"phrase",
"category"
],
"type": "string",
"description": "Default keyword. Each kind takes DIFFERENT filters — see the description."
},
"limit": {
"type": "number",
"description": "maximum number of suggestions to return"
},
"terms": {
"type": "array",
"items": {
"type": "string"
},
"description": "kind:\"keyword\" ONLY. Seed search terms to expand from, e.g. [\"productivity\",\"task management\"]. Refused on phrase/category."
},
"offset": {
"type": "number",
"description": "suggestions to skip, for paging"
},
"phrases": {
"type": "array",
"items": {
"type": "string"
},
"description": "kind:\"phrase\" ONLY, and REQUIRED there on the SEARCH route — the phrases to look up, e.g. [\"best productivity app\"]."
},
"queryType": {
"enum": [
"SUGGESTION",
"SEARCH"
],
"type": "string",
"description": "phrase/category ONLY (refused on keyword). Default SEARCH. SUGGESTION is measured BROKEN at Apple — ~34s then a server error."
},
"categories": {
"type": "array",
"items": {
"type": "string"
},
"description": "kind:\"category\" ONLY, and REQUIRED there on the SEARCH route — the App Store categories to look up, e.g. [\"Utilities\"]."
},
"promotedObjectId": {
"type": "string",
"description": "REQUIRED — the app’s adamId, or an Apple Maps brandId."
},
"countriesOrRegions": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO 3166-1 alpha-2, e.g. [\"US\",\"GB\"]."
},
"promotedObjectType": {
"enum": [
"APPSTORE_APP",
"BUSINESS_BRAND"
],
"type": "string",
"description": "Default APPSTORE_APP."
}
}
}| fields |
| array |
| limit the returned fields. Omit for all of them. |
| offset | number | row to start at (default 0). hasMore is now MEASURED against Apple’s own pagination.totalCount rather than inferred from a full page, and the default ordering is localSpend DESCENDING — so omitted rows are the LOW-SPEND tail. |
| endTime* | string | YYYY-MM-DD (required) |
| groupBy | array | restricted per level — see the description; an unsupported value is refused by name |
| timeZone | string | ORTZ = the organization time zone, Apple’s default. Search-term reports accept ORTZ only. |
| adGroupId | string | optional second filter — v5 had no path for this |
| startTime* | string | YYYY-MM-DD (required) |
| campaignId | string | REQUIRED for every level except campaign. It moved from the URL into a filter in the Platform API, but Apple still enforces it — only a campaign-level report may be account-wide. |
| grandTotals | boolean | add a summary row across all result rows |
| granularity | string | omit entirely for a single day, or for totals only |
| emptyMetrics | boolean | include entities with no delivery. App Store reports only, and never together with groupBy. |
| promotedObjectType | string | apps = App Store (default), business-brands = Ads on Apple Maps |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"startTime",
"endTime"
],
"properties": {
"level": {
"enum": [
"campaign",
"adgroup",
"keyword",
"searchterm",
"ad"
],
"type": "string",
"description": "default campaign"
},
"limit": {
"type": "number",
"description": "rows per page, up to 5000 (default 100)"
},
"fields": {
"type": "array",
"items": {
"type": "string"
},
"description": "limit the returned fields. Omit for all of them."
},
"offset": {
"type": "number",
"description": "row to start at (default 0). hasMore is now MEASURED against Apple’s own pagination.totalCount rather than inferred from a full page, and the default ordering is localSpend DESCENDING — so omitted rows are the LOW-SPEND tail."
},
"endTime": {
"type": "string",
"description": "YYYY-MM-DD (required)"
},
"groupBy": {
"type": "array",
"items": {
"type": "string"
},
"description": "restricted per level — see the description; an unsupported value is refused by name"
},
"timeZone": {
"enum": [
"ORTZ",
"UTC"
],
"type": "string",
"description": "ORTZ = the organization time zone, Apple’s default. Search-term reports accept ORTZ only."
},
"adGroupId": {
"type": "string",
"description": "optional second filter — v5 had no path for this"
},
"startTime": {
"type": "string",
"description": "YYYY-MM-DD (required)"
},
"campaignId": {
"type": "string",
"description": "REQUIRED for every level except campaign. It moved from the URL into a filter in the Platform API, but Apple still enforces it — only a campaign-level report may be account-wide."
},
"grandTotals": {
"type": "boolean",
"description": "add a summary row across all result rows"
},
"granularity": {
"enum": [
"HOURLY",
"DAILY",
"WEEKLY",
"MONTHLY"
],
"type": "string",
"description": "omit entirely for a single day, or for totals only"
},
"emptyMetrics": {
"type": "boolean",
"description": "include entities with no delivery. App Store reports only, and never together with groupBy."
},
"promotedObjectType": {
"enum": [
"apps",
"business-brands"
],
"type": "string",
"description": "apps = App Store (default), business-brands = Ads on Apple Maps"
}
}
}| granularity | string | Default WEEKLY_SUN_SAT. DAILY does NOT exist on this report. |
| countryOrRegion | array | ISO 3166-1 alpha-2, e.g. ["US","GB","DE"]. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"start",
"end"
],
"properties": {
"end": {
"type": "string",
"description": "REQUIRED YYYY-MM-DD."
},
"genre": {
"type": "array",
"items": {
"type": "string"
},
"description": "App Store genre names, free text, e.g. [\"PRODUCTIVITY_UTILITIES\"]."
},
"limit": {
"type": "number",
"description": "maximum number of rows to return"
},
"start": {
"type": "string",
"description": "REQUIRED YYYY-MM-DD. Must be a SUNDAY when granularity is WEEKLY_SUN_SAT."
},
"offset": {
"type": "number",
"description": "rows to skip, for paging"
},
"granularity": {
"enum": [
"WEEKLY_SUN_SAT",
"MONTHLY"
],
"type": "string",
"description": "Default WEEKLY_SUN_SAT. DAILY does NOT exist on this report."
},
"countryOrRegion": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO 3166-1 alpha-2, e.g. [\"US\",\"GB\",\"DE\"]."
}
}
}| notZero |
| boolean |
| drop rows whose metrics are all zero |
| attribution | string | web only, default click |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1–10000, default 1000"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD, default 7 days ago"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD, default today"
},
"cohort": {
"type": "boolean",
"description": "attribute to impression day"
},
"having": {
"type": "string",
"description": "server-side numeric filter, e.g. \"cost > 0 AND roas_7d < 1\": report columns, numbers, > < >= <= = !=, AND, OR, parentheses (AppLovin warns it slows the report)"
},
"offset": {
"type": "number",
"description": "rows to skip, for paging past the limit"
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "e.g. day, campaign, campaign_id_external, creative_set, country, impressions, clicks, ctr, cost, sales, roas_7d, chka_usd_7d"
},
"filters": {
"type": "object",
"description": "column -> values",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"notZero": {
"type": "boolean",
"description": "drop rows whose metrics are all zero"
},
"attribution": {
"enum": [
"click",
"click_and_view"
],
"type": "string",
"description": "web only, default click"
}
}
}| Default daily_budget. |
| amount | string | Override Apple’s suggestion with your own decimal amount, e.g. "500.00". Omit to apply what Apple suggested. |
| confirm | boolean | REQUIRED true. Without it nothing changes and the refusal states the campaign and the exact budget or CPA change. |
| 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. |
| promotedObjectId* | string | REQUIRED — the same app or brand id the recommendation was queried for. |
| promotedObjectType | string | Default APPSTORE_APP. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id",
"promotedObjectId"
],
"properties": {
"id": {
"type": "string",
"description": "REQUIRED — the recommendation id from list_apple_ads_recommendations."
},
"type": {
"enum": [
"daily_budget",
"target_cpa"
],
"type": "string",
"description": "Default daily_budget."
},
"amount": {
"type": "string",
"description": "Override Apple’s suggestion with your own decimal amount, e.g. \"500.00\". Omit to apply what Apple suggested."
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true. Without it nothing changes and the refusal states the campaign and the exact budget or CPA change."
},
"currency": {
"type": "string",
"description": "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."
},
"promotedObjectId": {
"type": "string",
"description": "REQUIRED — the same app or brand id the recommendation was queried for."
},
"promotedObjectType": {
"enum": [
"APPSTORE_APP",
"BUSINESS_BRAND"
],
"type": "string",
"description": "Default APPSTORE_APP."
}
}
}| older name for adAccountId (MCP only) |
| audienceId* | string | the customer list to write into |
| acceptTerms | boolean | REQUIRED true — the user must agree to Microsoft’s Customer Match terms first. Without it nothing is uploaded and nothing is hashed |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"action": {
"enum": [
"Add",
"Remove",
"Replace"
],
"type": "string",
"description": "Add (default) is additive across calls"
},
"emails": {
"type": "array",
"items": {
"type": "string"
},
"description": "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"
},
"subType": {
"enum": [
"Email"
],
"type": "string",
"description": "Email is the only identifier Microsoft supports here — it publishes Phone and CRM as “Not currently supported”"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"audienceId": {
"type": "string",
"description": "the customer list to write into"
},
"acceptTerms": {
"type": "boolean",
"description": "REQUIRED true — the user must agree to Microsoft’s Customer Match terms first. Without it nothing is uploaded and nothing is hashed"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}| older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| confirmCount | number | echo back how many recommendations this applies: the exact count the un-confirmed call reported |
| recommendationIds* | array | ids from microsoft_ads_recommendations, max 100. There is deliberately no "apply everything" |
| confirmCostIncrease | number | echo back Microsoft’s own summed cost estimate for them: the exact number the un-confirmed call reported |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"recommendationIds"
],
"properties": {
"dryRun": {
"type": "boolean",
"description": "report what would happen and apply nothing at all"
},
"confirm": {
"type": "boolean",
"description": "the user has SEEN the pre-flight list and said yes. Not sufficient on its own, both echo numbers below are also required"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"confirmCount": {
"type": "number",
"description": "echo back how many recommendations this applies: the exact count the un-confirmed call reported"
},
"recommendationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ids from microsoft_ads_recommendations, max 100. There is deliberately no \"apply everything\""
},
"confirmCostIncrease": {
"type": "number",
"description": "echo back Microsoft’s own summed cost estimate for them: the exact number the un-confirmed call reported"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId",
"adgroupIds",
"actionMode"
],
"properties": {
"usageMode": {
"enum": [
"Include",
"Exclude"
],
"type": "string",
"description": "REQUIRED when actionMode is Apply, and refused with Disconnect"
},
"actionMode": {
"enum": [
"Apply",
"Disconnect"
],
"type": "string",
"description": "Apply attaches the audience to the ad groups; Disconnect removes it (case-sensitive)"
},
"adgroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "from list_tiktok_ads_campaigns"
},
"audienceId": {
"type": "string",
"description": "exactly one — TikTok takes a single custom_audience_id per call"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"formId"
],
"properties": {
"formId": {
"type": "string",
"description": "the form to retire (from list_meta_lead_forms)"
},
"pageId": {
"type": "string",
"description": "which connected Page (required only if the profile has more than one)"
},
"status": {
"enum": [
"ARCHIVED",
"ACTIVE"
],
"type": "string",
"description": "default ARCHIVED; \"ACTIVE\" restores a form that was archived"
},
"confirm": {
"type": "boolean",
"description": "must be true to actually change it — without it nothing changes and you get the read-back to show the user"
}
}
}| the objects to bind or unbind |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"ruleId",
"dimension",
"dimensionIds",
"bindType"
],
"properties": {
"ruleId": {
"type": "string",
"description": "from list_tiktok_ads_rules"
},
"confirm": {
"type": "boolean",
"description": "required to BIND onto a live rule that can spend"
},
"bindType": {
"enum": [
"BIND",
"UNBIND"
],
"type": "string",
"description": "BIND puts the objects under the rule; UNBIND takes them out"
},
"dimension": {
"enum": [
"CAMPAIGN",
"ADGROUP",
"AD"
],
"type": "string",
"description": "level of the objects in dimensionIds: CAMPAIGN, ADGROUP or AD"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"confirmScope": {
"type": "string",
"description": "required in the same case — the scopeToken from list_tiktok_ads_rules"
},
"dimensionIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the objects to bind or unbind"
}
}
}| string |
| YYYY-MM-DD — required for keyword and related modes |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"q",
"country",
"language"
],
"properties": {
"q": {
"type": "string",
"description": "the keyword to look up, e.g. \"running shoes\""
},
"mode": {
"type": "string",
"description": "keyword (default) | related | history"
},
"country": {
"type": "string",
"description": "REQUIRED, e.g. \"us\" — there is no default and one will not be guessed"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD — required for keyword and related modes"
},
"language": {
"type": "string",
"description": "REQUIRED, e.g. \"en-US\""
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD — required for keyword and related modes"
}
}
}| currency | string | ISO 4217 for bids; defaults to the ad account’s currency. |
| allowPartialSuccess | boolean | true keeps the items that succeeded when others fail. Apple’s default is all-or-nothing. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"items"
],
"properties": {
"op": {
"enum": [
"create",
"update"
],
"type": "string",
"description": "Default create."
},
"kind": {
"enum": [
"keyword",
"negative_keyword"
],
"type": "string",
"description": "Default keyword."
},
"items": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "REQUIRED. create: [{adGroupId,text,matchType,bid?}] for keywords, or [{campaignId|adGroupId,text,matchType}] for negatives. update: [{id,bid?,status?}]."
},
"confirm": {
"type": "boolean",
"description": "Required only when the target ad group or campaign is already ENABLED, where the new keywords could serve immediately."
},
"currency": {
"type": "string",
"description": "ISO 4217 for bids; defaults to the ad account’s currency."
},
"allowPartialSuccess": {
"type": "boolean",
"description": "true keeps the items that succeeded when others fail. Apple’s default is all-or-nothing."
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"pack": {
"type": "string",
"description": "the pack id to buy (e.g. pack-2k) — omit to list the available packs first"
},
"confirm": {
"type": "boolean",
"description": "set true to actually charge the saved card for `pack` (required for the one-click charge; ignored on the checkout-link path)"
},
"quote_token": {
"type": "string",
"description": "the quoteToken returned by the quote step — REQUIRED (with confirm:true) to charge; it binds the exact pack + price you quoted (10-minute validity) and makes a retried confirm idempotent"
}
}
}| compatibilityFilter | string | narrow the answer — COMPATIBLE returns only the fields that CAN be added |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"metrics": {
"type": "array",
"items": {
"type": "string"
},
"description": "the same metrics you would send to analytics_report (defaults to the same three it defaults to)"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id"
},
"dimensions": {
"type": "array",
"items": {
"type": "string"
},
"description": "the same dimensions you would send to analytics_report"
},
"metricFilter": {
"type": "object",
"description": "the same GA4 FilterExpression you would send to analytics_report",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"dimensionFilter": {
"type": "object",
"description": "the same GA4 FilterExpression you would send to analytics_report",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"compatibilityFilter": {
"enum": [
"COMPATIBILITY_UNSPECIFIED",
"COMPATIBLE",
"INCOMPATIBLE"
],
"type": "string",
"description": "narrow the answer — COMPATIBLE returns only the fields that CAN be added"
}
}
}| string |
| the connector id: applovin_ads, stripe, intercom, mailchimp, klaviyo, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, wordpress, slack, discord, webhook |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"provider"
],
"properties": {
"fields": {
"type": "object",
"description": "that provider's own field names and values, e.g. {\"apiKey\":\"…\"}; the names for each provider are in the description",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"provider": {
"type": "string",
"description": "the connector id: applovin_ads, stripe, intercom, mailchimp, klaviyo, vercel, openai_ads, apple_ads, bluesky, telegram, bing_webmaster, posthog, mixpanel, amplitude, wordpress, slack, discord, webhook"
}
}
}| details | string | longer body text for the annotation |
| category | string | an EXISTING category name in Amplitude — they answer 404 for one that does not exist |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"label",
"start"
],
"properties": {
"end": {
"type": "string",
"description": "ISO 8601 — only for an annotation that spans a period"
},
"label": {
"type": "string",
"description": "the title shown on the chart, e.g. \"Meta: summer UGC campaign live\""
},
"start": {
"type": "string",
"description": "ISO 8601 WITH time and offset, e.g. '2026-08-18T14:30:00+00:00' — a bare YYYYMMDD is refused"
},
"chartId": {
"type": "string",
"description": "omit for a GLOBAL annotation that appears on every chart in the project"
},
"details": {
"type": "string",
"description": "longer body text for the annotation"
},
"category": {
"type": "string",
"description": "an EXISTING category name in Amplitude — they answer 404 for one that does not exist"
}
}
}| GA4 AudienceFilterClause list — each {clauseType:INCLUDE|EXCLUDE, simpleFilter|sequenceFilter}. See the worked example in the description |
| exclusionDurationMode | string | IMMUTABLE, and it applies to EVERY exclude clause on the audience |
| membershipDurationDays* | number | REQUIRED and IMMUTABLE, 1-540. 30 is the usual remarketing window; 540 is GA4's maximum |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"displayName",
"description",
"membershipDurationDays",
"filterClauses"
],
"properties": {
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id"
},
"description": {
"type": "string",
"description": "REQUIRED by Google on the Audience resource — one line saying who is in it"
},
"displayName": {
"type": "string",
"description": "the audience name shown in GA4 and in Google Ads"
},
"eventTrigger": {
"type": "object",
"description": "optional {eventName, logCondition} event GA4 logs when a user joins the audience",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"filterClauses": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "GA4 AudienceFilterClause list — each {clauseType:INCLUDE|EXCLUDE, simpleFilter|sequenceFilter}. See the worked example in the description"
},
"exclusionDurationMode": {
"enum": [
"EXCLUDE_TEMPORARILY",
"EXCLUDE_PERMANENTLY"
],
"type": "string",
"description": "IMMUTABLE, and it applies to EVERY exclude clause on the audience"
},
"membershipDurationDays": {
"type": "number",
"description": "REQUIRED and IMMUTABLE, 1-540. 30 is the usual remarketing window; 540 is GA4's maximum"
}
}
}| the event parameter GA4 already collects, e.g. "customer_tier" — NOT the report label |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"parameterName"
],
"properties": {
"scope": {
"enum": [
"EVENT",
"USER",
"ITEM"
],
"type": "string",
"description": "default EVENT. The 50-dimension cap applies to EVENT scope"
},
"property": {
"type": "string",
"description": "the NUMERIC GA4 property id from list_analytics_properties — it must be one SHARED with this profile, and any other is refused by name"
},
"description": {
"type": "string",
"description": "optional description, ≤150 characters"
},
"displayName": {
"type": "string",
"description": "the label shown in GA4 reports — defaults to parameterName"
},
"parameterName": {
"type": "string",
"description": "the event parameter GA4 already collects, e.g. \"customer_tier\" — NOT the report label"
}
}
}| parameterName* |
| 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 |
| measurementUnit* | string | REQUIRED — Google marks it Required and there is no safe default |
| restrictedMetricType | array | REQUIRED when measurementUnit is CURRENCY, and refused for every other unit |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property",
"parameterName",
"measurementUnit"
],
"properties": {
"scope": {
"enum": [
"EVENT"
],
"type": "string",
"description": "EVENT is the only scope GA4 accepts for a metric, and it is immutable"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id from list_analytics_properties — never the G-XXXXXXX Measurement ID"
},
"description": {
"type": "string",
"description": "≤150 characters"
},
"displayName": {
"type": "string",
"description": "the label shown in reports; defaults to parameterName, max 82 characters"
},
"parameterName": {
"type": "string",
"description": "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"
},
"measurementUnit": {
"enum": [
"STANDARD",
"CURRENCY",
"FEET",
"METERS",
"KILOMETERS",
"MILES",
"MILLISECONDS",
"SECONDS",
"MINUTES",
"HOURS"
],
"type": "string",
"description": "REQUIRED — Google marks it Required and there is no safe default"
},
"restrictedMetricType": {
"type": "array",
"items": {
"enum": [
"COST_DATA",
"REVENUE_DATA"
],
"type": "string"
},
"description": "REQUIRED when measurementUnit is CURRENCY, and refused for every other unit"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"creativeId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "REQUIRED."
},
"adGroupId": {
"type": "string",
"description": "REQUIRED."
},
"creativeId": {
"type": "string",
"description": "REQUIRED — from create_apple_ads_creative."
}
}
}| targeting |
| object |
| Who and where this ad group reaches. For Apple Maps, locationGroupIds is what says which shops it promotes. |
| campaignId* | string | REQUIRED. |
| defaultBid | string | Max cost per tap, e.g. "1.50". |
| searchMatch | boolean | Opt into Apple’s automated keyword matching. |
| pricingModel | string | Default CPT; must match the campaign billing event. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "REQUIRED."
},
"endTime": {
"type": "string",
"description": "Omit to inherit the campaign end date."
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code of the ad group bids"
},
"startTime": {
"type": "string",
"description": "Defaults to now."
},
"targeting": {
"type": "object",
"properties": {
"gender": {
"type": "array",
"items": {
"enum": [
"M",
"F"
],
"type": "string"
},
"description": "App Store."
},
"maxAge": {
"type": "number",
"description": "App Store. 18-64; omit entirely to reach 65+."
},
"minAge": {
"type": "number",
"description": "App Store. 18-64."
},
"radius": {
"enum": [
"CLOSE",
"MEDIUM",
"FAR"
],
"type": "string",
"description": "Apple Maps — how near a person must be. Cannot be combined with adminArea/locality/postalCode."
},
"daypart": {
"type": "array",
"items": {
"type": "number"
},
"description": "Hour slots 0-167, slot 0 = Sunday 12am, in the ad account time zone."
},
"countryIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Country ids from the Geo API (App Store)."
},
"deviceClass": {
"type": "array",
"items": {
"enum": [
"IPHONE",
"IPAD"
],
"type": "string"
},
"description": "App Store."
},
"localityIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "City ids from the Geo API."
},
"adminAreaIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "State/province ids from the Geo API."
},
"postalCodeIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Postal code ids from the Geo API (Apple Maps)."
},
"appCategoryIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "App Store. 100 means \"the same category as my app\"."
},
"appDownloaderIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "App Store — reach people who downloaded these Adam IDs."
},
"locationGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "APPLE MAPS — the location groups whose shops this ad group promotes. Max 25."
},
"excludeAppCategoryIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "App Store category ids to exclude from reach"
},
"excludeAppDownloaderIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "App Store — suppress people who already have your app."
}
},
"description": "Who and where this ad group reaches. For Apple Maps, locationGroupIds is what says which shops it promotes."
},
"campaignId": {
"type": "string",
"description": "REQUIRED."
},
"defaultBid": {
"type": "string",
"description": "Max cost per tap, e.g. \"1.50\"."
},
"searchMatch": {
"type": "boolean",
"description": "Opt into Apple’s automated keyword matching."
},
"pricingModel": {
"enum": [
"CPT",
"CPM",
"CPA"
],
"type": "string",
"description": "Default CPT; must match the campaign billing event."
}
}
}| startTime* | string | REQUIRED. Must be TOMORROW or later (midnight UTC) — Apple rejects today. |
| clientName | string | client name shown on the invoice |
| invoiceName | string | name shown on the invoice |
| orderNumber | string | purchase order or internal order number |
| billingEmail* | string | REQUIRED. |
| primaryBuyerName | string | name of the primary buyer on the invoice |
| primaryBuyerEmail* | string | REQUIRED — Apple bills and notifies this address. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"startTime",
"amount",
"primaryBuyerEmail",
"billingEmail"
],
"properties": {
"name": {
"type": "string",
"description": "REQUIRED."
},
"amount": {
"type": "string",
"description": "REQUIRED total budget as a decimal string, e.g. \"50000.00\"."
},
"endTime": {
"type": "string",
"description": "Omit for an open-ended budget."
},
"currency": {
"type": "string",
"description": "ISO 4217; defaults to the ad account’s currency, which it must match."
},
"startTime": {
"type": "string",
"description": "REQUIRED. Must be TOMORROW or later (midnight UTC) — Apple rejects today."
},
"clientName": {
"type": "string",
"description": "client name shown on the invoice"
},
"invoiceName": {
"type": "string",
"description": "name shown on the invoice"
},
"orderNumber": {
"type": "string",
"description": "purchase order or internal order number"
},
"billingEmail": {
"type": "string",
"description": "REQUIRED."
},
"primaryBuyerName": {
"type": "string",
"description": "name of the primary buyer on the invoice"
},
"primaryBuyerEmail": {
"type": "string",
"description": "REQUIRED — Apple bills and notifies this address."
}
}
}| string |
| REQUIRED. App Store id of the promoted app, e.g. "6752439949". |
| countries* | array | REQUIRED. ISO 3166-1 alpha-2 codes, e.g. ["US","CA"]. |
| startTime | string | campaign start time, ISO 8601 |
| placements | array | 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. |
| dailyBudget* | string | REQUIRED. Decimal string, e.g. "25.00". |
| budgetOrderIds | array | 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. |
| bidStrategyType | string | Default MANUAL_CPT. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"appAdamId",
"dailyBudget",
"countries"
],
"properties": {
"bid": {
"type": "string",
"description": "MAX_CONVERSIONS only — the target CPA. Refused on MANUAL_CPT."
},
"name": {
"type": "string",
"description": "REQUIRED. Unique across the entire Apple Ads organization."
},
"endTime": {
"type": "string",
"description": "Omit to run indefinitely."
},
"currency": {
"type": "string",
"description": "Defaults to the connected ad account’s currency; an account cannot mix currencies."
},
"appAdamId": {
"type": "string",
"description": "REQUIRED. App Store id of the promoted app, e.g. \"6752439949\"."
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED. ISO 3166-1 alpha-2 codes, e.g. [\"US\",\"CA\"]."
},
"startTime": {
"type": "string",
"description": "campaign start time, ISO 8601"
},
"placements": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"dailyBudget": {
"type": "string",
"description": "REQUIRED. Decimal string, e.g. \"25.00\"."
},
"budgetOrderIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"bidStrategyType": {
"enum": [
"MANUAL_CPT",
"MAX_CONVERSIONS"
],
"type": "string",
"description": "Default MANUAL_CPT."
}
}
}| mapsBrandId | string | Apple Maps only — the brand this creative belongs to. |
| creativeType | string | Default DEFAULT_PRODUCT_PAGE. |
| defaultLocale | string | Apple Maps only, e.g. "en-US". |
| localizedText | object | Apple Maps only — promo copy per locale, e.g. {"en-US":"Visit us today"}. |
| productPageId | string | REQUIRED for CUSTOM_PRODUCT_PAGE; refused on DEFAULT_PRODUCT_PAGE, which is the app’s standard listing. |
| creativeSubtype | string | Apple Maps only. Default BUSINESS_LOGO. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "REQUIRED."
},
"adamId": {
"type": "string",
"description": "REQUIRED for App Store creatives — the App Store id."
},
"brandId": {
"type": "string",
"description": "older name for mapsBrandId (MCP only)"
},
"assetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Apple Maps only — ids from upload_apple_ads_asset."
},
"mapsBrandId": {
"type": "string",
"description": "Apple Maps only — the brand this creative belongs to."
},
"creativeType": {
"enum": [
"DEFAULT_PRODUCT_PAGE",
"CUSTOM_PRODUCT_PAGE",
"LOCAL_ADS_SEARCH_CREATIVE"
],
"type": "string",
"description": "Default DEFAULT_PRODUCT_PAGE."
},
"defaultLocale": {
"type": "string",
"description": "Apple Maps only, e.g. \"en-US\"."
},
"localizedText": {
"type": "object",
"description": "Apple Maps only — promo copy per locale, e.g. {\"en-US\":\"Visit us today\"}.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"productPageId": {
"type": "string",
"description": "REQUIRED for CUSTOM_PRODUCT_PAGE; refused on DEFAULT_PRODUCT_PAGE, which is the app’s standard listing."
},
"creativeSubtype": {
"enum": [
"BUSINESS_LOGO",
"BUSINESS_ASSET"
],
"type": "string",
"description": "Apple Maps only. Default BUSINESS_LOGO."
}
}
}| locationIds |
| array |
| REQUIRED for STATIC — ids from list_apple_ads_locations. |
| mapsBrandId | string | REQUIRED: from list_apple_ads_brands. Immutable afterwards. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "REQUIRED."
},
"rules": {
"type": "array",
"items": {
"type": "object",
"required": [
"field",
"operator",
"value"
],
"properties": {
"field": {
"enum": [
"adminArea",
"locality",
"postalCode",
"locationId"
],
"type": "string",
"description": "adminArea (full state name, e.g. California), locality (countryOrRegion|adminArea|locality), postalCode or locationId"
},
"value": {
"description": "string for EQUALS or NOT_EQUALS; array for IN or NOT_IN"
},
"operator": {
"enum": [
"EQUALS",
"NOT_EQUALS",
"IN",
"NOT_IN"
],
"type": "string",
"description": "EQUALS, NOT_EQUALS, IN or NOT_IN"
}
}
},
"description": "REQUIRED for DYNAMIC. value is a string for EQUALS/NOT_EQUALS and an array for IN/NOT_IN."
},
"brandId": {
"type": "string",
"description": "older name for mapsBrandId (MCP only)"
},
"groupType": {
"enum": [
"STATIC",
"DYNAMIC"
],
"type": "string",
"description": "Default STATIC. Immutable afterwards."
},
"description": {
"type": "string",
"description": "description of the location group"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED for STATIC — ids from list_apple_ads_locations."
},
"mapsBrandId": {
"type": "string",
"description": "REQUIRED: from list_apple_ads_brands. Immutable afterwards."
}
}
}| usMetros |
| array |
| US metro names such as NEW_YORK; not with usRegions |
| catalogId | string | product catalog id for DPA or DOA campaigns |
| countries* | array | ISO codes, e.g. ["US","CA"] |
| goalValue | number | target cost (CPE/CPP/CPI) or ROAS ratio (0.9 = 90%) |
| startDate | string | YYYY-MM-DD, default tomorrow (UTC) |
| usRegions | array | US state codes, only with US |
| websiteUrl | string | WEB: full landing page URL with tracking parameters |
| catalogType | string | product catalog type: DPA or DOA |
| dailyBudget | number | account currency per day, all countries |
| eventTarget | string | CPE: the event, e.g. GENERATE_LEAD |
| packageName | string | app package name, for app campaigns |
| countryGoals | object | per-country goal values keyed by ISO code, e.g. {US: 2.5} |
| variantSetId | string | product variant set id within the catalog |
| impressionUrl | string | impression-tracking URL from the measurement partner, for app campaigns |
| roasDayTarget | string | WEB DAY0/DAY7; APP DAY7/DAY28 |
| countryBudgets | object | per country instead, e.g. {"US":50} |
| trackingMethod | string | mobile measurement partner that tracks installs: ADJUST, APPSFLYER, APSALAR, BRANCH, KOCHAVA or TENJIN |
| biddingStrategy | string | automatic bidding billed per thousand impressions, or target goal billed per install |
| audienceStrategy | string | CPP / CHK_ROAS only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"countries",
"goalType"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"type": {
"enum": [
"WEB",
"APP"
],
"type": "string",
"description": "default: the connected account’s type"
},
"endDate": {
"type": "string",
"description": "end date, YYYY-MM-DD"
},
"clickUrl": {
"type": "string",
"description": "click-tracking URL from the measurement partner, for app campaigns"
},
"goalType": {
"enum": [
"CPE",
"CPP",
"CHK_ROAS",
"CPI",
"AD_ROAS",
"BLD_ROAS"
],
"type": "string",
"description": "WEB: CPE, CPP, CHK_ROAS"
},
"itunesId": {
"type": "number",
"description": "iOS App Store numeric id, for iOS app campaigns"
},
"platform": {
"enum": [
"IOS",
"ANDROID"
],
"type": "string",
"description": "app platform: IOS or ANDROID"
},
"usMetros": {
"type": "array",
"items": {
"type": "string"
},
"description": "US metro names such as NEW_YORK; not with usRegions"
},
"catalogId": {
"type": "string",
"description": "product catalog id for DPA or DOA campaigns"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO codes, e.g. [\"US\",\"CA\"]"
},
"goalValue": {
"type": "number",
"description": "target cost (CPE/CPP/CPI) or ROAS ratio (0.9 = 90%)"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD, default tomorrow (UTC)"
},
"usRegions": {
"type": "array",
"items": {
"type": "string"
},
"description": "US state codes, only with US"
},
"websiteUrl": {
"type": "string",
"description": "WEB: full landing page URL with tracking parameters"
},
"catalogType": {
"enum": [
"DPA",
"DOA"
],
"type": "string",
"description": "product catalog type: DPA or DOA"
},
"dailyBudget": {
"type": "number",
"description": "account currency per day, all countries"
},
"eventTarget": {
"type": "string",
"description": "CPE: the event, e.g. GENERATE_LEAD"
},
"packageName": {
"type": "string",
"description": "app package name, for app campaigns"
},
"countryGoals": {
"type": "object",
"description": "per-country goal values keyed by ISO code, e.g. {US: 2.5}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "number"
}
},
"variantSetId": {
"type": "string",
"description": "product variant set id within the catalog"
},
"impressionUrl": {
"type": "string",
"description": "impression-tracking URL from the measurement partner, for app campaigns"
},
"roasDayTarget": {
"enum": [
"DAY0",
"DAY7",
"DAY28"
],
"type": "string",
"description": "WEB DAY0/DAY7; APP DAY7/DAY28"
},
"countryBudgets": {
"type": "object",
"description": "per country instead, e.g. {\"US\":50}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "number"
}
},
"trackingMethod": {
"enum": [
"ADJUST",
"APPSFLYER",
"APSALAR",
"BRANCH",
"KOCHAVA",
"TENJIN"
],
"type": "string",
"description": "mobile measurement partner that tracks installs: ADJUST, APPSFLYER, APSALAR, BRANCH, KOCHAVA or TENJIN"
},
"biddingStrategy": {
"enum": [
"AUTO_BIDDING_WITH_CPM_BILLING",
"TARGET_GOAL_WITH_CPI_BILLING"
],
"type": "string",
"description": "automatic bidding billed per thousand impressions, or target goal billed per install"
},
"audienceStrategy": {
"enum": [
"UNIVERSAL",
"PROSPECTING",
"DISCOVERY"
],
"type": "string",
"description": "CPP / CHK_ROAS only"
}
}
}| true = go back to the account-level brand info |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"assetIds"
],
"properties": {
"url": {
"type": "string",
"description": "landing page for this set only (WEB)"
},
"name": {
"type": "string",
"description": "creative set name"
},
"assetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "AppLovin asset ids"
},
"brandInfo": {
"type": "object",
"required": [
"title",
"description",
"buttonLabel",
"buttonColor",
"iconAssetId"
],
"properties": {
"title": {
"type": "string",
"description": "≤25 characters"
},
"buttonColor": {
"type": "string",
"description": "\"#RRGGBB\""
},
"buttonLabel": {
"type": "string",
"description": "CTA, ≤13 characters"
},
"description": {
"type": "string",
"description": "≤75 characters"
},
"iconAssetId": {
"type": "string",
"description": "an uploaded IMG_ICON asset id"
}
},
"description": "override the account brand on THIS creative set — how a brand A/B tests the end card"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO country codes for the creative set"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "e.g. ENGLISH"
},
"campaignId": {
"type": "string",
"description": "campaign to place the creative set in; optional"
},
"useAccountBrandInfo": {
"type": "boolean",
"description": "true = go back to the account-level brand info"
}
}
}| string |
| the ad group this ad lives in |
| finalUrls | array | the landing page(s) — at least one is required |
| headlines | array | 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). |
| customerId | string | omit to use the brand’s selected default account |
| logoImages | array | DISPLAY only — logo asset resource names |
| businessName | string | DISPLAY only — ≤25 characters |
| descriptions | array | SEARCH: 2–4 descriptions, each ≤90 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to pin it. |
| longHeadline | string | DISPLAY only — ≤90 characters |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
| marketingImages | array | DISPLAY only — landscape 1.91:1 asset resource names from upload_google_ads_asset |
| squareMarketingImages | array | DISPLAY only — square 1:1 asset resource names |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId"
],
"properties": {
"path1": {
"type": "string",
"description": "SEARCH only — display-URL path segment, ≤15 chars"
},
"path2": {
"type": "string",
"description": "SEARCH only — second display-URL path segment, ≤15 chars"
},
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and create nothing"
},
"status": {
"enum": [
"ENABLED",
"PAUSED"
],
"type": "string",
"description": "default PAUSED"
},
"confirm": {
"type": "boolean",
"description": "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"
},
"adGroupId": {
"type": "string",
"description": "the ad group this ad lives in"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the landing page(s) — at least one is required"
},
"headlines": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"pin": {
"enum": [
"HEADLINE_1",
"HEADLINE_2",
"HEADLINE_3",
"DESCRIPTION_1",
"DESCRIPTION_2"
],
"type": "string"
},
"text": {
"type": "string"
}
}
}
]
},
"description": "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)."
},
"customerId": {
"type": "string",
"description": "omit to use the brand’s selected default account"
},
"logoImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — logo asset resource names"
},
"businessName": {
"type": "string",
"description": "DISPLAY only — ≤25 characters"
},
"descriptions": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"pin": {
"enum": [
"HEADLINE_1",
"HEADLINE_2",
"HEADLINE_3",
"DESCRIPTION_1",
"DESCRIPTION_2"
],
"type": "string"
},
"text": {
"type": "string"
}
}
}
]
},
"description": "SEARCH: 2–4 descriptions, each ≤90 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to pin it."
},
"longHeadline": {
"type": "string",
"description": "DISPLAY only — ≤90 characters"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
},
"marketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — landscape 1.91:1 asset resource names from upload_google_ads_asset"
},
"squareMarketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — square 1:1 asset resource names"
}
}
}| max CPC for this ad group — omit to inherit the campaign bidding |
| campaignId* | string | the campaign this ad group belongs to |
| customerId | string | omit to use the brand’s selected default account |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name"
],
"properties": {
"ad": {
"type": "object",
"properties": {
"path1": {
"type": "string",
"description": "SEARCH only — display-URL path segment, ≤15 chars"
},
"path2": {
"type": "string",
"description": "SEARCH only — second display-URL path segment, ≤15 chars"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the landing page(s) — at least one is required"
},
"headlines": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"pin": {
"enum": [
"HEADLINE_1",
"HEADLINE_2",
"HEADLINE_3",
"DESCRIPTION_1",
"DESCRIPTION_2"
],
"type": "string"
},
"text": {
"type": "string"
}
}
}
]
},
"description": "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)."
},
"logoImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — logo asset resource names"
},
"businessName": {
"type": "string",
"description": "DISPLAY only — ≤25 characters"
},
"descriptions": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"pin": {
"enum": [
"HEADLINE_1",
"HEADLINE_2",
"HEADLINE_3",
"DESCRIPTION_1",
"DESCRIPTION_2"
],
"type": "string"
},
"text": {
"type": "string"
}
}
}
]
},
"description": "SEARCH: 2–4 descriptions, each ≤90 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to pin it."
},
"longHeadline": {
"type": "string",
"description": "DISPLAY only — ≤90 characters"
},
"marketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — landscape 1.91:1 asset resource names from upload_google_ads_asset"
},
"squareMarketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — square 1:1 asset resource names"
}
},
"description": "build the ad in the same atomic call"
},
"name": {
"type": "string",
"description": "ad group name"
},
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and create nothing"
},
"status": {
"enum": [
"ENABLED",
"PAUSED"
],
"type": "string",
"description": "default PAUSED"
},
"confirm": {
"type": "boolean",
"description": "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"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "≤80 characters, ≤10 words"
},
"paused": {
"type": "boolean",
"description": "true = create this keyword paused, so it does not bid"
},
"negative": {
"type": "boolean",
"description": "true = BLOCK this term instead of targeting it"
},
"cpcBidUsd": {
"type": "number",
"description": "per-keyword max CPC"
},
"matchType": {
"enum": [
"EXACT",
"PHRASE",
"BROAD"
],
"type": "string",
"description": "default PHRASE"
}
}
},
"description": "a SEARCH ad group with no keywords never shows"
},
"cpcBidUsd": {
"type": "number",
"description": "max CPC for this ad group — omit to inherit the campaign bidding"
},
"campaignId": {
"type": "string",
"description": "the campaign this ad group belongs to"
},
"customerId": {
"type": "string",
"description": "omit to use the brand’s selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
}
}
}| languages |
| array |
| ISO language codes, e.g. ["en","fr"] |
| locations | array | location NAMES to target, e.g. ["United States"] or ["Toronto","Vancouver"]. WITHOUT this the campaign runs WORLDWIDE — the most expensive default in Google Ads |
| startDate | string | YYYY-MM-DD |
| customerId | string | 10-digit account id (from list_google_ads_campaigns) — omit to use the brand’s selected default account |
| channelType | string | default SEARCH |
| dailyBudgetUsd | number | daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) — creates a budget inline; required unless budgetResourceName is given |
| 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 |
| searchPartners | boolean | SEARCH only — also serve on Google search partners (default false) |
| loginCustomerId | string | manager id if operating through an MCC |
| excludedLocations | array | location names to EXCLUDE |
| budgetResourceName | string | reuse an existing budget instead of creating one |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"dryRun": {
"type": "boolean",
"description": "validate the WHOLE tree against Google without creating anything. Nothing is written and no budget is consumed"
},
"adGroup": {
"type": "object",
"properties": {
"ad": {
"type": "object",
"properties": {
"path1": {
"type": "string",
"description": "SEARCH only — display-URL path segment, ≤15 chars"
},
"path2": {
"type": "string",
"description": "SEARCH only — second display-URL path segment, ≤15 chars"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the landing page(s) — at least one is required"
},
"headlines": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"pin": {
"enum": [
"HEADLINE_1",
"HEADLINE_2",
"HEADLINE_3",
"DESCRIPTION_1",
"DESCRIPTION_2"
],
"type": "string"
},
"text": {
"type": "string"
}
}
}
]
},
"description": "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)."
},
"logoImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — logo asset resource names"
},
"businessName": {
"type": "string",
"description": "DISPLAY only — ≤25 characters"
},
"descriptions": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"pin": {
"enum": [
"HEADLINE_1",
"HEADLINE_2",
"HEADLINE_3",
"DESCRIPTION_1",
"DESCRIPTION_2"
],
"type": "string"
},
"text": {
"type": "string"
}
}
}
]
},
"description": "SEARCH: 2–4 descriptions, each ≤90 characters. DISPLAY: 1–5. Pass a plain string, or {text, pin} to pin it."
},
"longHeadline": {
"type": "string",
"description": "DISPLAY only — ≤90 characters"
},
"marketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — landscape 1.91:1 asset resource names from upload_google_ads_asset"
},
"squareMarketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "DISPLAY only — square 1:1 asset resource names"
}
},
"description": "the ad to build in the ad group: finalUrls, headlines, descriptions, path1, path2"
},
"name": {
"type": "string",
"description": "ad group name"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "≤80 characters, ≤10 words"
},
"paused": {
"type": "boolean",
"description": "true = create this keyword paused, so it does not bid"
},
"negative": {
"type": "boolean",
"description": "true = BLOCK this term instead of targeting it"
},
"cpcBidUsd": {
"type": "number",
"description": "per-keyword max CPC"
},
"matchType": {
"enum": [
"EXACT",
"PHRASE",
"BROAD"
],
"type": "string",
"description": "default PHRASE"
}
}
},
"description": "keywords for the ad group as a list of {text, matchType, negative, cpcBidUsd, paused}"
},
"cpcBidUsd": {
"type": "number",
"description": "max CPC for the ad group in the ACCOUNT’s currency (historical name); omit to inherit the campaign bidding"
}
},
"description": "build the serving tree in the same atomic call — WITHOUT this you get a campaign shell that can never show an ad"
},
"bidding": {
"type": "object",
"required": [
"strategy"
],
"properties": {
"strategy": {
"enum": [
"MANUAL_CPC",
"MAXIMIZE_CLICKS",
"MAXIMIZE_CONVERSIONS",
"MAXIMIZE_CONVERSION_VALUE",
"TARGET_CPA",
"TARGET_ROAS"
],
"type": "string",
"description": "bidding strategy; default MANUAL_CPC. TARGET_CPA needs targetCpaUsd, TARGET_ROAS needs targetRoas"
},
"maxCpcUsd": {
"type": "number",
"description": "MAXIMIZE_CLICKS only — optional max CPC ceiling"
},
"targetRoas": {
"type": "number",
"description": "REQUIRED for TARGET_ROAS — e.g. 4 = $4 revenue per $1 spent"
},
"enhancedCpc": {
"type": "boolean",
"description": "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."
},
"targetCpaUsd": {
"type": "number",
"description": "REQUIRED for TARGET_CPA — cost per conversion you will pay"
}
},
"description": "how the campaign bids — default MANUAL_CPC"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO language codes, e.g. [\"en\",\"fr\"]"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "location NAMES to target, e.g. [\"United States\"] or [\"Toronto\",\"Vancouver\"]. WITHOUT this the campaign runs WORLDWIDE — the most expensive default in Google Ads"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"customerId": {
"type": "string",
"description": "10-digit account id (from list_google_ads_campaigns) — omit to use the brand’s selected default account"
},
"channelType": {
"enum": [
"SEARCH",
"DISPLAY"
],
"type": "string",
"description": "default SEARCH"
},
"dailyBudgetUsd": {
"type": "number",
"description": "daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) — creates a budget inline; required unless budgetResourceName is given"
},
"finalUrlSuffix": {
"type": "string",
"description": "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"
},
"searchPartners": {
"type": "boolean",
"description": "SEARCH only — also serve on Google search partners (default false)"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"excludedLocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "location names to EXCLUDE"
},
"budgetResourceName": {
"type": "string",
"description": "reuse an existing budget instead of creating one"
},
"trackingUrlTemplate": {
"type": "string",
"description": "click-tracking redirect, e.g. \"{lpurl}?src=x\" — only if you use a tracker"
},
"urlCustomParameters": {
"type": "object",
"description": "up to 8 custom parameters, used in URLs as {_key}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"containsEuPoliticalAds": {
"type": "boolean",
"description": "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"
}
}
}| customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| countingType | string | ONE_PER_CLICK for leads, MANY_PER_CLICK for sales — defaults by category |
| primaryForGoal | boolean | default true — whether this action is biddable for its category |
| defaultValueUsd | number | what one conversion is worth — required in practice for TARGET_ROAS |
| loginCustomerId | string | manager id if operating through an MCC |
| defaultCurrencyCode | string | 3-letter ISO code, e.g. USD |
| alwaysUseDefaultValue | boolean | ignore any value sent with the conversion and always use the default |
| viewThroughLookbackDays | number | 1–30 days |
| clickThroughLookbackDays | number | 1–90 days |
| includeInConversionsMetric | boolean | default true — false makes smart bidding IGNORE it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "what the user calls this result, e.g. \"Purchase\", \"Demo request\""
},
"type": {
"enum": [
"WEBPAGE",
"UPLOAD_CLICKS",
"UPLOAD_CALLS"
],
"type": "string",
"description": "default WEBPAGE — a conversion that happens on the website"
},
"dryRun": {
"type": "boolean",
"description": "validate against Google and create NOTHING"
},
"status": {
"enum": [
"ENABLED",
"PAUSED",
"REMOVED",
"HIDDEN"
],
"type": "string",
"description": "default ENABLED — anything else records nothing"
},
"category": {
"enum": [
"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"
],
"type": "string",
"description": "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"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"countingType": {
"enum": [
"ONE_PER_CLICK",
"MANY_PER_CLICK"
],
"type": "string",
"description": "ONE_PER_CLICK for leads, MANY_PER_CLICK for sales — defaults by category"
},
"primaryForGoal": {
"type": "boolean",
"description": "default true — whether this action is biddable for its category"
},
"defaultValueUsd": {
"type": "number",
"description": "what one conversion is worth — required in practice for TARGET_ROAS"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"defaultCurrencyCode": {
"type": "string",
"description": "3-letter ISO code, e.g. USD"
},
"alwaysUseDefaultValue": {
"type": "boolean",
"description": "ignore any value sent with the conversion and always use the default"
},
"viewThroughLookbackDays": {
"type": "number",
"description": "1–30 days"
},
"clickThroughLookbackDays": {
"type": "number",
"description": "1–90 days"
},
"includeInConversionsMetric": {
"type": "boolean",
"description": "default true — false makes smart bidding IGNORE it"
}
}
}| audiences |
| array |
| audience ids from manage_google_ads_audience |
| languages | array | ISO language codes to target, e.g. ["en"] |
| locations | array | place names to target, resolved by Google, e.g. United States |
| startDate | string | campaign start date, YYYY-MM-DD |
| customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
| countryCode | string | 2-letter country code that disambiguates a place name, e.g. CA for Toronto |
| dailyBudgetUsd | number | daily budget in the ACCOUNT’s currency (historical name, not necessarily USD), 1 to 100000; required unless budgetResourceName is given |
| finalUrlSuffix | string | UTM suffix appended to clicks; omit for Hermoso's default, empty string for none |
| loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
| excludedLocations | array | place names to exclude, resolved by Google |
| budgetResourceName | string | existing campaign budget resource name to reuse instead of creating a new budget |
| 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} |
| containsEuPoliticalAds | boolean | true only for genuine EU political advertising |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"ad"
],
"properties": {
"ad": {
"type": "object",
"required": [
"finalUrl",
"businessName",
"headlines",
"longHeadlines",
"descriptions"
],
"properties": {
"name": {
"type": "string",
"description": "name of the ad inside Google Ads (optional)"
},
"finalUrl": {
"type": "string",
"description": "landing page the ad opens, full http or https URL"
},
"headlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "short headlines, 1 to 5"
},
"logoImage": {
"type": "string",
"description": "square (1:1) logo image URL or Hermoso asset; used when logoAssets is empty"
},
"logoAssets": {
"type": "array",
"items": {
"type": "string"
},
"description": "square (1:1) logo asset resource names from upload_google_ads_asset; alternative to logoImage"
},
"videoAssets": {
"type": "array",
"items": {
"type": "string"
},
"description": "video asset resource names from upload_google_ads_video; one of this or youtubeVideoIds is required"
},
"businessName": {
"type": "string",
"description": "business name shown with the ad, 25 characters maximum"
},
"descriptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "ad descriptions, 1 to 5"
},
"longHeadlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "long headlines, 1 to 5"
},
"youtubeVideoIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "YouTube video ids of existing videos; alternative to videoAssets"
}
},
"description": "video ad: {videoAssets or youtubeVideoIds, logo, finalUrl, businessName, headlines, longHeadlines, descriptions}"
},
"name": {
"type": "string",
"description": "campaign name"
},
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and create nothing"
},
"bidding": {
"type": "object",
"required": [
"strategy"
],
"properties": {
"strategy": {
"enum": [
"MAXIMIZE_CONVERSIONS",
"TARGET_CPA",
"MAXIMIZE_CONVERSION_VALUE",
"TARGET_ROAS",
"MAXIMIZE_CLICKS"
],
"type": "string",
"description": "bidding strategy; defaults to MAXIMIZE_CONVERSIONS when omitted"
},
"maxCpcUsd": {
"type": "number",
"description": "maximum cost per click in the ACCOUNT’s currency"
},
"targetRoas": {
"type": "number",
"description": "target return as a ratio, e.g. 4 = $4 of revenue per $1 spent, for TARGET_ROAS"
},
"targetCpaUsd": {
"type": "number",
"description": "target cost per conversion in the ACCOUNT’s currency, for TARGET_CPA"
}
},
"description": "bidding object {strategy, targetCpaUsd, targetRoas, maxCpcUsd}; strategy defaults to MAXIMIZE_CONVERSIONS"
},
"endDate": {
"type": "string",
"description": "campaign end date, YYYY-MM-DD"
},
"channels": {
"type": "array",
"items": {
"enum": [
"youtubeShorts",
"youtubeInFeed",
"youtubeInStream",
"discover",
"gmail",
"display",
"maps"
],
"type": "string"
},
"description": "omit for all of Google's channels"
},
"audiences": {
"type": "array",
"items": {
"type": "string"
},
"description": "audience ids from manage_google_ads_audience"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO language codes to target, e.g. [\"en\"]"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "place names to target, resolved by Google, e.g. United States"
},
"startDate": {
"type": "string",
"description": "campaign start date, YYYY-MM-DD"
},
"customerId": {
"type": "string",
"description": "Google Ads account id, 10 digits (dashes ok); omit for the brand's default account"
},
"countryCode": {
"type": "string",
"description": "2-letter country code that disambiguates a place name, e.g. CA for Toronto"
},
"dailyBudgetUsd": {
"type": "number",
"description": "daily budget in the ACCOUNT’s currency (historical name, not necessarily USD), 1 to 100000; required unless budgetResourceName is given"
},
"finalUrlSuffix": {
"type": "string",
"description": "UTM suffix appended to clicks; omit for Hermoso's default, empty string for none"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id, when the account is managed through one"
},
"excludedLocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "place names to exclude, resolved by Google"
},
"budgetResourceName": {
"type": "string",
"description": "existing campaign budget resource name to reuse instead of creating a new budget"
},
"trackingUrlTemplate": {
"type": "string",
"description": "click-tracking redirect template, e.g. {lpurl}?src=x; only if you use a tracker"
},
"urlCustomParameters": {
"type": "object",
"description": "up to 8 custom URL parameters, used in URLs as {_key}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"containsEuPoliticalAds": {
"type": "boolean",
"description": "true only for genuine EU political advertising"
}
}
}| campaign name |
| dryRun | boolean | validate the whole tree against Google and create NOTHING |
| bidding | object | bidding {strategy, targetCpaUsd, targetRoas}; no manual bidding, strategy defaults to MAXIMIZE_CONVERSIONS |
| endDate | string | YYYY-MM-DD |
| feedLabel | string | optional, retail only: narrow to ONE Merchant Center feed by its feed label. Omit to use products from all feeds |
| languages | array | ISO codes, e.g. ["en"] |
| locations | array | place NAMES ("United States", "Toronto") — resolved for you |
| startDate | string | YYYY-MM-DD |
| assetGroup* | object | the creative — Google requires every field above before a PMax campaign can serve |
| customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| countryCode | string | 2-letter hint to disambiguate a city name |
| dailyBudgetUsd | number | daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) — or pass budgetResourceName |
| 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 |
| loginCustomerId | string | manager id if operating through an MCC |
| 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 |
| excludedLocations | array | places to block |
| budgetResourceName | string | an existing budget to reuse |
| 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} |
| 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 |
| containsEuPoliticalAds | boolean | true ONLY for genuine EU political advertising |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"assetGroup"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"dryRun": {
"type": "boolean",
"description": "validate the whole tree against Google and create NOTHING"
},
"bidding": {
"type": "object",
"properties": {
"strategy": {
"enum": [
"MAXIMIZE_CONVERSIONS",
"MAXIMIZE_CONVERSION_VALUE",
"TARGET_CPA",
"TARGET_ROAS"
],
"type": "string",
"description": "default MAXIMIZE_CONVERSIONS — PMax has no manual bidding"
},
"targetRoas": {
"type": "number",
"description": "required for TARGET_ROAS, e.g. 4 = $4 revenue per $1 spent"
},
"targetCpaUsd": {
"type": "number",
"description": "required for TARGET_CPA"
}
},
"description": "bidding {strategy, targetCpaUsd, targetRoas}; no manual bidding, strategy defaults to MAXIMIZE_CONVERSIONS"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"feedLabel": {
"type": "string",
"description": "optional, retail only: narrow to ONE Merchant Center feed by its feed label. Omit to use products from all feeds"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO codes, e.g. [\"en\"]"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "place NAMES (\"United States\", \"Toronto\") — resolved for you"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"assetGroup": {
"type": "object",
"required": [
"name",
"finalUrls",
"headlines",
"longHeadlines",
"descriptions",
"businessName",
"logoAssets",
"marketingImages",
"squareMarketingImages"
],
"properties": {
"name": {
"type": "string",
"description": "asset group name"
},
"path1": {
"type": "string",
"description": "display-URL path, ≤15 characters"
},
"path2": {
"type": "string",
"description": "second display-URL path, ≤15 characters"
},
"audiences": {
"type": "array",
"items": {
"type": "string"
},
"description": "signals: audience ids from manage_google_ads_audience(create_audience)"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the landing page(s) — at least one"
},
"headlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "3–15, each ≤30 characters"
},
"logoAssets": {
"type": "array",
"items": {
"type": "string"
},
"description": "at least one 1:1 LOGO asset resource name from upload_google_ads_asset"
},
"businessName": {
"type": "string",
"description": "≤25 characters"
},
"descriptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "2–5, each ≤90 characters"
},
"searchThemes": {
"type": "array",
"items": {
"type": "string"
},
"description": "signals: up to 50 search themes (keyword-like hints, ≤10 words each)"
},
"longHeadlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "1–5, each ≤90 characters"
},
"youtubeVideos": {
"type": "array",
"items": {
"type": "string"
},
"description": "optional YOUTUBE_VIDEO asset resource names"
},
"landscapeLogos": {
"type": "array",
"items": {
"type": "string"
},
"description": "optional 4:1 LANDSCAPE_LOGO asset resource names — LOGO + LANDSCAPE_LOGO may total at most 5"
},
"marketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "at least one 1.91:1 asset resource name"
},
"squareMarketingImages": {
"type": "array",
"items": {
"type": "string"
},
"description": "at least one 1:1 asset resource name"
}
},
"description": "the creative — Google requires every field above before a PMax campaign can serve"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"countryCode": {
"type": "string",
"description": "2-letter hint to disambiguate a city name"
},
"dailyBudgetUsd": {
"type": "number",
"description": "daily budget in the ACCOUNT’s currency (historical name — not necessarily USD; 1–100000) — or pass budgetResourceName"
},
"finalUrlSuffix": {
"type": "string",
"description": "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"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"merchantCenterId": {
"type": "string",
"description": "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"
},
"excludedLocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "places to block"
},
"budgetResourceName": {
"type": "string",
"description": "an existing budget to reuse"
},
"trackingUrlTemplate": {
"type": "string",
"description": "click-tracking redirect, e.g. \"{lpurl}?src=x\" — only if you use a tracker"
},
"urlCustomParameters": {
"type": "object",
"description": "up to 8 custom parameters, used in URLs as {_key}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"brandGuidelinesEnabled": {
"type": "boolean",
"description": "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"
},
"containsEuPoliticalAds": {
"type": "boolean",
"description": "true ONLY for genuine EU political advertising"
}
}
}| any other writable contact properties, by internal name |
| hs_lead_status | string | HubSpot lead status internal value, e.g. NEW |
| hubspotOwnerId | string | HubSpot owner id (digits) from list_hubspot_owners |
| lifecyclestage | string | HubSpot lifecycle stage internal value, e.g. lead or customer |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"email"
],
"properties": {
"email": {
"type": "string",
"description": "the contact email — the key HubSpot de-duplicates on"
},
"phone": {
"type": "string",
"description": "contact phone number"
},
"company": {
"type": "string",
"description": "company name on the contact"
},
"ownerId": {
"type": "string",
"description": "older name for hubspotOwnerId (MCP only)"
},
"website": {
"type": "string",
"description": "company website URL"
},
"lastname": {
"type": "string",
"description": "contact last name"
},
"firstname": {
"type": "string",
"description": "contact first name"
},
"properties": {
"type": "object",
"description": "any other writable contact properties, by internal name",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"hs_lead_status": {
"type": "string",
"description": "HubSpot lead status internal value, e.g. NEW"
},
"hubspotOwnerId": {
"type": "string",
"description": "HubSpot owner id (digits) from list_hubspot_owners"
},
"lifecyclestage": {
"type": "string",
"description": "HubSpot lifecycle stage internal value, e.g. lead or customer"
}
}
}| status |
| string |
| default DRAFT |
| confirm | boolean | REQUIRED true to create it ACTIVE under a LIVE campaign group |
| country | string | campaign locale country, default US — this is the ad UI language market, NOT geo targeting. NEVER derive it from where the ad should run: targeting Canada still uses the US/en locale (LinkedIn refuses en_CA). Leave it alone unless the user explicitly asks for a different interface language. |
| endDate | string | YYYY-MM-DD; omit for an open-ended run |
| exclude | object | facets to exclude, same shape |
| include | object | further targeting facets ANDed onto locations, e.g. {titles:[…], industries:[…], seniorities:[…], staffCountRanges:[…]} |
| costType | string | CPM / CPC / CPV |
| language | string | campaign locale language, default en |
| unitCost | number | the bid |
| locations | array | REQUIRED unless targetingCriteria is given — geo URNs or bare geo ids from search_linkedin_ads_targeting |
| startDate | string | YYYY-MM-DD; defaults to today |
| adAccountId | string | LinkedIn ad account id from list_linkedin_ads_campaigns |
| dailyBudget | number | daily budget in the ad account currency |
| runSchedule | object | raw LinkedIn runSchedule object; startDate and endDate are simpler |
| totalBudget | number | total budget in the ad account currency; optional |
| currencyCode | string | must match the ad account currency, e.g. USD |
| objectiveType | string | LinkedIn objective, e.g. WEBSITE_VISIT |
| organizationId | string | the LinkedIn company Page this campaign advertises — LinkedIn REQUIRES it for Sponsored Content, Dynamic and Lead Gen campaigns |
| campaignGroupId* | string | the campaign group this campaign lives in — LinkedIn requires one |
| politicalIntent | string | POLITICAL, NOT_POLITICAL or NOT_DECLARED; LinkedIn political-ads declaration |
| excludeLocations | array | geo URNs or ids to exclude from targeting |
| targetingCriteria | object | LinkedIn’s raw targeting object — passed through and overrides locations/include/exclude |
| offsiteDeliveryEnabled | boolean | also serve on the LinkedIn Audience Network; default false |
| optimizationTargetType | string | LinkedIn optimization target value, passed through as LinkedIn names it |
| audienceExpansionEnabled | boolean | true lets LinkedIn expand delivery beyond the targeted audience |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignGroupId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"type": {
"type": "string",
"description": "LinkedIn campaign type, e.g. SPONSORED_UPDATES"
},
"format": {
"type": "string",
"description": "LinkedIn creative format value, passed through to LinkedIn"
},
"locale": {
"type": "object",
"description": "LinkedIn locale object (country, language); leave unset for the default en/US",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"status": {
"enum": [
"DRAFT",
"ACTIVE",
"PAUSED",
"ARCHIVED"
],
"type": "string",
"description": "default DRAFT"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create it ACTIVE under a LIVE campaign group"
},
"country": {
"type": "string",
"description": "campaign locale country, default US — this is the ad UI language market, NOT geo targeting. NEVER derive it from where the ad should run: targeting Canada still uses the US/en locale (LinkedIn refuses en_CA). Leave it alone unless the user explicitly asks for a different interface language."
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD; omit for an open-ended run"
},
"exclude": {
"type": "object",
"description": "facets to exclude, same shape",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"include": {
"type": "object",
"description": "further targeting facets ANDed onto locations, e.g. {titles:[…], industries:[…], seniorities:[…], staffCountRanges:[…]}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"costType": {
"type": "string",
"description": "CPM / CPC / CPV"
},
"language": {
"type": "string",
"description": "campaign locale language, default en"
},
"unitCost": {
"type": "number",
"description": "the bid"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED unless targetingCriteria is given — geo URNs or bare geo ids from search_linkedin_ads_targeting"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD; defaults to today"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id from list_linkedin_ads_campaigns"
},
"dailyBudget": {
"type": "number",
"description": "daily budget in the ad account currency"
},
"runSchedule": {
"type": "object",
"description": "raw LinkedIn runSchedule object; startDate and endDate are simpler",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"totalBudget": {
"type": "number",
"description": "total budget in the ad account currency; optional"
},
"currencyCode": {
"type": "string",
"description": "must match the ad account currency, e.g. USD"
},
"objectiveType": {
"type": "string",
"description": "LinkedIn objective, e.g. WEBSITE_VISIT"
},
"organizationId": {
"type": "string",
"description": "the LinkedIn company Page this campaign advertises — LinkedIn REQUIRES it for Sponsored Content, Dynamic and Lead Gen campaigns"
},
"campaignGroupId": {
"type": "string",
"description": "the campaign group this campaign lives in — LinkedIn requires one"
},
"politicalIntent": {
"enum": [
"POLITICAL",
"NOT_POLITICAL",
"NOT_DECLARED"
],
"type": "string",
"description": "POLITICAL, NOT_POLITICAL or NOT_DECLARED; LinkedIn political-ads declaration"
},
"excludeLocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "geo URNs or ids to exclude from targeting"
},
"targetingCriteria": {
"type": "object",
"description": "LinkedIn’s raw targeting object — passed through and overrides locations/include/exclude",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"offsiteDeliveryEnabled": {
"type": "boolean",
"description": "also serve on the LinkedIn Audience Network; default false"
},
"optimizationTargetType": {
"type": "string",
"description": "LinkedIn optimization target value, passed through as LinkedIn names it"
},
"audienceExpansionEnabled": {
"type": "boolean",
"description": "true lets LinkedIn expand delivery beyond the targeted audience"
}
}
}| totalBudget | number | optional group-level total budget, in the ad account’s currency |
| currencyCode | string | must match the ad account’s currency or LinkedIn refuses it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "campaign group name"
},
"status": {
"enum": [
"DRAFT",
"ACTIVE",
"PAUSED",
"ARCHIVED"
],
"type": "string",
"description": "default DRAFT"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create it ACTIVE"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD — REQUIRED by LinkedIn whenever totalBudget is set"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD — when campaigns under this group may start; defaults to today"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id from list_linkedin_ads_campaigns"
},
"runSchedule": {
"type": "object",
"description": "LinkedIn raw runSchedule object, passed through; startDate/endDate are easier",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"totalBudget": {
"type": "number",
"description": "optional group-level total budget, in the ad account’s currency"
},
"currencyCode": {
"type": "string",
"description": "must match the ad account’s currency or LinkedIn refuses it"
}
}
}| postUrn | string | Sponsor an EXISTING post — urn:li:share:… / urn:li:ugcPost:… |
| imageUrl | string | a Hermoso-hosted image to attach — a render, or the user’s OWN creative put through upload_file first (an arbitrary external host is refused) |
| videoUrl | string | a Hermoso-hosted video to attach — a render, or the user’s own footage via upload_file |
| campaignId* | string | the campaign this ad belongs to |
| leadButton | string | the lead gen button label, default LEARN_MORE (UNLOCK_FULL_DOCUMENT for a gated document) |
| leadFormId | string | LEAD GEN ADS: the lead gen form this ad opens. REQUIRED under a LEAD_GENERATION campaign and refused under any other objective |
| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
| documentUrl | string | DOCUMENT AD: a PDF / PPTX / DOCX shown as a swipeable document in the feed — Hermoso-hosted, so upload_file it first |
| landingPage | string | WHERE THE AD SENDS THE CLICK — a full https URL, sent as LinkedIn’s contentLandingPage. REQUIRED when the campaign objective is WEBSITE_VISIT (refused here, for free, without it). Only for a NEW sponsored post: an existing postUrn keeps its own link. |
| allowReshare | boolean | true lets viewers reshare the sponsored post; default false (reshare disabled) |
| callToAction | string | the BUTTON on the ad (contentCallToActionLabel) — needs landingPage. Omit for no button. |
| intendedStatus | string | default DRAFT |
| organizationId | string | the company Page that authors the Direct Sponsored Content post; omit only when the connection administers exactly one Page |
| gatedPreviewPages | number | document ad under a LEAD_GENERATION campaign only: pages shown free before the lead form unlocks the rest |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"name": {
"type": "string",
"description": "the creative’s name in Campaign Manager"
},
"text": {
"type": "string",
"description": "the ad copy"
},
"title": {
"type": "string",
"description": "title of an uploaded document or video ad (max 200 characters)"
},
"altText": {
"type": "string",
"description": "image alt text for accessibility, for a single-image ad"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create it ACTIVE under a LIVE campaign (real spend)"
},
"postUrn": {
"type": "string",
"description": "Sponsor an EXISTING post — urn:li:share:… / urn:li:ugcPost:…"
},
"imageUrl": {
"type": "string",
"description": "a Hermoso-hosted image to attach — a render, or the user’s OWN creative put through upload_file first (an arbitrary external host is refused)"
},
"videoUrl": {
"type": "string",
"description": "a Hermoso-hosted video to attach — a render, or the user’s own footage via upload_file"
},
"campaignId": {
"type": "string",
"description": "the campaign this ad belongs to"
},
"leadButton": {
"enum": [
"APPLY",
"DOWNLOAD",
"VIEW_QUOTE",
"LEARN_MORE",
"SIGN_UP",
"SUBSCRIBE",
"REGISTER",
"REQUEST_DEMO",
"JOIN",
"ATTEND",
"UNLOCK_FULL_DOCUMENT",
"SHOP_NOW",
"BUY_NOW",
"SEE_MORE"
],
"type": "string",
"description": "the lead gen button label, default LEARN_MORE (UNLOCK_FULL_DOCUMENT for a gated document)"
},
"leadFormId": {
"type": "string",
"description": "LEAD GEN ADS: the lead gen form this ad opens. REQUIRED under a LEAD_GENERATION campaign and refused under any other objective"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
},
"documentUrl": {
"type": "string",
"description": "DOCUMENT AD: a PDF / PPTX / DOCX shown as a swipeable document in the feed — Hermoso-hosted, so upload_file it first"
},
"landingPage": {
"type": "string",
"description": "WHERE THE AD SENDS THE CLICK — a full https URL, sent as LinkedIn’s contentLandingPage. REQUIRED when the campaign objective is WEBSITE_VISIT (refused here, for free, without it). Only for a NEW sponsored post: an existing postUrn keeps its own link."
},
"allowReshare": {
"type": "boolean",
"description": "true lets viewers reshare the sponsored post; default false (reshare disabled)"
},
"callToAction": {
"enum": [
"APPLY",
"DOWNLOAD",
"VIEW_QUOTE",
"LEARN_MORE",
"SIGN_UP",
"SUBSCRIBE",
"REGISTER",
"JOIN",
"ATTEND",
"REQUEST_DEMO",
"SEE_MORE",
"BUY_NOW",
"SHOP_NOW"
],
"type": "string",
"description": "the BUTTON on the ad (contentCallToActionLabel) — needs landingPage. Omit for no button."
},
"intendedStatus": {
"enum": [
"DRAFT",
"ACTIVE",
"PAUSED",
"ARCHIVED"
],
"type": "string",
"description": "default DRAFT"
},
"organizationId": {
"type": "string",
"description": "the company Page that authors the Direct Sponsored Content post; omit only when the connection administers exactly one Page"
},
"gatedPreviewPages": {
"type": "number",
"description": "document ad under a LEAD_GENERATION campaign only: pages shown free before the lead form unlocks the rest"
}
}
}| how conversions are credited across campaigns: last touch by campaign or by conversion |
| associateAllCampaigns | boolean | attach to up to 200 ACTIVE campaigns now — ask the user first |
| associateCampaignsByObjective | boolean | attach only to campaigns whose objective matches this conversion type |
| postClickAttributionWindowSize | number | 1, 7, 30 or 90 days (365 for the five long-window types). LinkedIn default 30. |
| viewThroughAttributionWindowSize | number | same allowed values. LinkedIn default 7. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"type"
],
"properties": {
"name": {
"type": "string",
"description": "shown in Campaign Manager and in every report"
},
"type": {
"type": "string",
"description": "the conversion behaviour, e.g. LEAD, PURCHASE, SIGN_UP, QUALIFIED_LEAD, KEY_PAGE_VIEW"
},
"enabled": {
"type": "boolean",
"description": "default true. A disabled rule REFUSES streamed events."
},
"valueType": {
"enum": [
"DYNAMIC",
"FIXED",
"NO_VALUE"
],
"type": "string",
"description": "DYNAMIC (default) takes each event’s own value"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
},
"attributionType": {
"enum": [
"LAST_TOUCH_BY_CAMPAIGN",
"LAST_TOUCH_BY_CONVERSION"
],
"type": "string",
"description": "how conversions are credited across campaigns: last touch by campaign or by conversion"
},
"associateAllCampaigns": {
"type": "boolean",
"description": "attach to up to 200 ACTIVE campaigns now — ask the user first"
},
"associateCampaignsByObjective": {
"type": "boolean",
"description": "attach only to campaigns whose objective matches this conversion type"
},
"postClickAttributionWindowSize": {
"type": "number",
"description": "1, 7, 30 or 90 days (365 for the five long-window types). LinkedIn default 30."
},
"viewThroughAttributionWindowSize": {
"type": "number",
"description": "same allowed values. LinkedIn default 7."
}
}
}| the fields to ask, in order: predefined names LinkedIn prefills from the member profile (FIRST_NAME, LAST_NAME, EMAIL, WORK_EMAIL, PHONE_NUMBER, COMPANY_NAME, JOB_TITLE, SENIORITY, COMPANY_SIZE, INDUSTRY, CITY, COUNTRY, ZIP_CODE, LINKEDIN_PROFILE_LINK …; friendly spellings like "work email" work) and/or custom questions {question, type:"text", maxLength?} or {question, options:[2–15 choices]} |
| adAccountId | string | own the form by this ad account |
| description | string | form description shown under the headline (optional) |
| thankYouCta | string | thank-you button label: visit website, learn more, view now, download or try now |
| hiddenFields | object | tracking values carried on every lead, {name: value}, max 20 |
| legalDisclaimer | string | 1–2000 characters |
| thankYouMessage | string | message shown to the person after they submit the form |
| privacyPolicyUrl | string | your privacy policy URL — LinkedIn shows it on the form |
| thankYouLandingPage | string | where the thank-you button sends people |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"headline",
"questions"
],
"properties": {
"name": {
"type": "string",
"description": "internal name, shown in Campaign Manager"
},
"state": {
"enum": [
"PUBLISHED",
"DRAFT"
],
"type": "string",
"description": "default PUBLISHED"
},
"locale": {
"type": "string",
"description": "default en_US"
},
"pageId": {
"type": "string",
"description": "Or own it by a company Page"
},
"consents": {
"type": "array",
"items": {},
"description": "consent checkbox texts, or {text, required:true}"
},
"headline": {
"type": "string",
"description": "the title of the form people see"
},
"questions": {
"type": "array",
"items": {},
"description": "the fields to ask, in order: predefined names LinkedIn prefills from the member profile (FIRST_NAME, LAST_NAME, EMAIL, WORK_EMAIL, PHONE_NUMBER, COMPANY_NAME, JOB_TITLE, SENIORITY, COMPANY_SIZE, INDUSTRY, CITY, COUNTRY, ZIP_CODE, LINKEDIN_PROFILE_LINK …; friendly spellings like \"work email\" work) and/or custom questions {question, type:\"text\", maxLength?} or {question, options:[2–15 choices]}"
},
"adAccountId": {
"type": "string",
"description": "own the form by this ad account"
},
"description": {
"type": "string",
"description": "form description shown under the headline (optional)"
},
"thankYouCta": {
"enum": [
"VISIT_COMPANY_WEBSITE",
"LEARN_MORE",
"VIEW_NOW",
"DOWNLOAD_NOW",
"TRY_NOW"
],
"type": "string",
"description": "thank-you button label: visit website, learn more, view now, download or try now"
},
"hiddenFields": {
"type": "object",
"description": "tracking values carried on every lead, {name: value}, max 20",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"legalDisclaimer": {
"type": "string",
"description": "1–2000 characters"
},
"thankYouMessage": {
"type": "string",
"description": "message shown to the person after they submit the form"
},
"privacyPolicyUrl": {
"type": "string",
"description": "your privacy policy URL — LinkedIn shows it on the form"
},
"thankYouLandingPage": {
"type": "string",
"description": "where the thank-you button sends people"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"displayName"
],
"properties": {
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "CLDR territory codes the items may be shown in"
},
"feedLabel": {
"type": "string",
"description": "market bucket, e.g. \"US\" — max 20 chars, A-Z/0-9/dashes. IMMUTABLE, and must be set together with contentLanguage"
},
"displayName": {
"type": "string",
"description": "the name the merchant will see in Merchant Center"
},
"destinations": {
"type": "array",
"items": {
"type": "string"
},
"description": "omit to inherit the account's own program participation (SHOPPING_ADS, FREE_LISTINGS, …)"
},
"contentLanguage": {
"type": "string",
"description": "two-letter ISO 639-1, e.g. \"en\". IMMUTABLE, and must be set together with feedLabel"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}| percentOff | number | REQUIRED for PERCENT_OFF — a plain number, e.g. 20 |
| promotionId* | string | YOUR OWN id for the promotion, e.g. 'black-friday-2026' |
| promotionUrl | string | the page on the merchant's site where the promotion shows |
| targetCountry* | string | two-letter CLDR territory code, e.g. US. Part of the promotion identity and not changeable later |
| moneyOffAmount | object | REQUIRED for MONEY_OFF — {"amountMicros":"10000000","currencyCode":"USD"} is $10 off |
| contentLanguage* | string | two-letter ISO 639-1 code, e.g. en. Must match the language of the products |
| couponValueType* | string | the kind of discount |
| customAttributes | array | merchant-provided attributes, {name, value} |
| merchantCenterId* | string | from list_merchant_accounts |
| redemptionChannel* | array | at least one; ONLINE for a webshop sale |
| eventApplicability | string | creates a SALES EVENT instead of a product promotion — a different thing |
| productApplicability | string | required unless eventApplicability is set — Google allows exactly one of the two |
| genericRedemptionCode | string | REQUIRED when offerType is GENERIC_CODE |
| promotionDestinations* | array | at least one, e.g. ['SHOPPING_ADS'] for paid Shopping or ['FREE_LISTINGS'] for the unpaid surface |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"promotionId",
"targetCountry",
"contentLanguage",
"longTitle",
"couponValueType",
"offerType",
"redemptionChannel",
"promotionDestinations",
"startDate",
"endDate"
],
"properties": {
"endDate": {
"type": "string",
"description": "ISO date; widened to the END of that day, because Google's interval end is exclusive"
},
"longTitle": {
"type": "string",
"description": "the promotion text shoppers see, e.g. '20% off all winter coats' — describe the offer, do not name the campaign"
},
"offerType": {
"enum": [
"NO_CODE",
"GENERIC_CODE"
],
"type": "string",
"description": "NO_CODE when the discount applies automatically, GENERIC_CODE when the shopper types a code"
},
"startDate": {
"type": "string",
"description": "ISO date, e.g. 2026-11-27"
},
"attributes": {
"type": "object",
"description": "any other promotion attribute Google publishes — item/brand/product-type filters, minimum purchase, store filters, display period — forwarded as given",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"percentOff": {
"type": "number",
"description": "REQUIRED for PERCENT_OFF — a plain number, e.g. 20"
},
"promotionId": {
"type": "string",
"description": "YOUR OWN id for the promotion, e.g. 'black-friday-2026'"
},
"promotionUrl": {
"type": "string",
"description": "the page on the merchant's site where the promotion shows"
},
"targetCountry": {
"type": "string",
"description": "two-letter CLDR territory code, e.g. US. Part of the promotion identity and not changeable later"
},
"moneyOffAmount": {
"type": "object",
"description": "REQUIRED for MONEY_OFF — {\"amountMicros\":\"10000000\",\"currencyCode\":\"USD\"} is $10 off",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"contentLanguage": {
"type": "string",
"description": "two-letter ISO 639-1 code, e.g. en. Must match the language of the products"
},
"couponValueType": {
"enum": [
"MONEY_OFF",
"PERCENT_OFF",
"BUY_M_GET_N_MONEY_OFF",
"BUY_M_GET_N_PERCENT_OFF",
"BUY_M_GET_MONEY_OFF",
"BUY_M_GET_PERCENT_OFF",
"FREE_GIFT",
"FREE_GIFT_WITH_VALUE",
"FREE_GIFT_WITH_ITEM_ID",
"FREE_SHIPPING_STANDARD",
"FREE_SHIPPING_OVERNIGHT",
"FREE_SHIPPING_TWO_DAY",
"MONEY_OFF_RANGE"
],
"type": "string",
"description": "the kind of discount"
},
"customAttributes": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "merchant-provided attributes, {name, value}"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
},
"redemptionChannel": {
"type": "array",
"items": {
"enum": [
"IN_STORE",
"ONLINE"
],
"type": "string"
},
"description": "at least one; ONLINE for a webshop sale"
},
"eventApplicability": {
"enum": [
"SITEWIDE",
"SPECIFIC_CATEGORIES"
],
"type": "string",
"description": "creates a SALES EVENT instead of a product promotion — a different thing"
},
"productApplicability": {
"enum": [
"ALL_PRODUCTS",
"SPECIFIC_PRODUCTS"
],
"type": "string",
"description": "required unless eventApplicability is set — Google allows exactly one of the two"
},
"genericRedemptionCode": {
"type": "string",
"description": "REQUIRED when offerType is GENERIC_CODE"
},
"promotionDestinations": {
"type": "array",
"items": {
"type": "string"
},
"description": "at least one, e.g. ['SHOPPING_ADS'] for paid Shopping or ['FREE_LISTINGS'] for the unpaid surface"
}
}
}| link | string | destination URL (defaults to the brand domain) |
| name | string | base name for the campaign/ad set/ads |
| cards | array | carousel cards in order — each may set its own headline/description/link |
| format | string | auto = one ad per asset (image or video); carousel = ONE multi-card ad |
| pageId | string | Omit when the brand has one Page; with several the call lists them, so ask the user which one. |
| adSetId | string | attach the ad(s) to an EXISTING ad set (skips ad-set creation) |
| country | string | 2-letter shorthand when you are not passing full targeting (default US) |
| endTime | string | REQUIRED with lifetimeBudgetUsd |
| message | string | primary ad text |
| minRoas | number | REQUIRED for LOWEST_COST_WITH_MIN_ROAS, e.g. 1.1 |
| pixelId | string | Meta Pixel id — with this the ad set optimizes for a real CONVERSION instead of falling back to link clicks |
| urlTags | string | boolean | UTM click tracking Meta appends to every click (the creative's url_tags). DEFAULT ON: utm_source=meta&utm_medium=paid&utm_campaign={{campaign.name}}&utm_term={{adset.name}}&utm_content={{ad.name}} (Meta fills the {{…}} per click) — skipped automatically when your link already has utm_ parameters. Pass your own query string to use it exactly as written, or false to send none. Not used on lead-form or click-to-message ads. The reply reads back what Meta stored. |
| dsaPayor | string | EU AD TRANSPARENCY: who pays for the ads (Meta shows it on the ad). Omit it and it matches dsaBeneficiary |
| headline | string | headline |
| imageUrl | string | public https image URL for the ad creative |
| videoUrl | string | a video URL -> a real Meta VIDEO ad (uploaded + transcoded + thumbnailed for you) |
| imageUrls | array | several image URLs -> one ad each, or the carousel cards in order |
| objective | string | default OUTCOME_TRAFFIC |
| startTime | string | ISO-8601 with offset, e.g. 2026-08-01T09:00:00-0700 |
| targeting | object | full Meta ad-set targeting — age, gender, geo, interests, behaviours, audiences, languages, placements, devices. Use EXACTLY these key names: an unrecognised one (e.g. geoLocations) is REFUSED by name — it is never dropped, because a dropped geo key used to fall back to targeting the United States. |
| campaignId | string | attach to an existing campaign instead of creating one |
| leadFormId | string | LEAD ADS — attach an instant form (from list_meta_lead_forms / create_meta_lead_form) so the button opens that form INSIDE Facebook/Instagram instead of sending the click to a website. Requires objective:"OUTCOME_LEADS"; the ad set is then optimized for LEAD_GENERATION and pointed at the form automatically. Read the submissions afterwards with read_meta_leads. |
| adAccountId* | string | Ad account id |
| bidStrategy | string | bidding: lowest cost, bid cap, cost cap or minimum ROAS (Meta's strategy names) |
| boostPostId | string | Promote a post that ALREADY EXISTS instead of building a new ad from media. Accepts the numeric post id, <pageId>_<postId>, or a permalink (an Instagram post is its NUMERIC media id — an instagram.com link carries only a shortcode, which Meta cannot resolve). Cannot be combined with image/video inputs, and creative fields do not apply — a boost shows the post as published. |
| boostTarget | string | Which surface the boosted post lives on. Default facebook. REQUIRED for an Instagram post: an IG media id and a Facebook post id are both bare digits, so this is never inferred — Meta takes a different creative for each (object_story_id for a Page post; object_id + instagram_user_id + source_instagram_media_id for an IG post). |
| description | string | the smaller description line under the headline |
| bidAmountUsd | number | REQUIRED for a bid cap / cost cap |
| billingEvent | string | default IMPRESSIONS |
| productSetId | string | CATALOG (dynamic product) ADS. Meta binds a catalog at TWO levels and they are different ids on different objects: the CAMPAIGN takes productCatalogId, the AD SET takes productSetId. Passing a CATALOG id here is refused by name rather than moved, because Meta’s own error for a mis-placed id names neither the level nor the right field. Cannot be combined with pixelId — an ad set has exactly one promoted_object — and its conversion event is conversionEvent, which Meta defaults to PURCHASE. |
| thumbnailUrl | string | custom video thumbnail (otherwise Meta picks a frame) |
| adsetSchedule | array | dayparting — minutes from midnight (0–1440), days 0=Sunday…6=Saturday |
| applicationId | string | app-promotion ads |
| dailyBudgetUsd | number | ad-set daily budget in the AD ACCOUNT's currency (historical name — not necessarily USD; default 10, or 1,000 in a whole-unit currency like JPY) — spends only once ACTIVE. Under a campaign that HAS its own budget (create_meta_campaign with dailyBudgetUsd, Advantage+ campaign budget) leave this and lifetimeBudgetUsd OUT: Meta allows a budget at the campaign or the ad set, never both, so the ad set then gets none and shares the campaign’s, and naming one is refused by name before anything is created |
| dsaBeneficiary | string | EU AD TRANSPARENCY: the person or organisation this ad promotes (Meta shows it on the ad). Meta requires it whenever the ad set reaches the EU; omit it and Hermoso uses the ad account's own default, else this profile's brand name, else the Page name, and the summary says which was used |
| objectStoreUrl | string | app store link for an app-install ad |
| attributionSpec | array | e.g. [{event_type:"CLICK_THROUGH",window_days:7}] |
| carouselEndCard | boolean | append the Page end card to a carousel |
| conversionEvent | string | PURCHASE | LEAD | COMPLETE_REGISTRATION | ADD_TO_CART | INITIATED_CHECKOUT | … |
| destinationType | string | WHERE A CLICK LANDS, and worth deciding for a merchant with a shop. "WEBSITE" sends it to their own site — their pixel fires, their email capture runs, their upsell flow works. "WEBSITE_AND_SHOP" lets Meta route it into the in-app Facebook/Instagram Shop instead: often fewer taps to a purchase, but the visit never reaches their site. FROM META API v26 AN ADVERTISER WITH A SHOP DEFAULTS TO WEBSITE_AND_SHOP, so pass "WEBSITE_AND_SHOP_OPT_OUT" to keep every click on their own site. Also accepts APP, MESSENGER, INSTAGRAM_DIRECT, WHATSAPP, ON_AD, ON_POST, ON_PAGE, ON_EVENT, ON_VIDEO, SHOP_AUTOMATIC. An unknown value is refused by name, and the read-back says in words where clicks will go. CLICK-TO-WHATSAPP ADS (certified against Meta 2026-09-03): pass destinationType "WHATSAPP", cta "WHATSAPP_MESSAGE" and optimizationGoal "CONVERSATIONS" (objective OUTCOME_ENGAGEMENT). Meta refuses the ad set unless the Page already has a WhatsApp number linked (Facebook Page settings > WhatsApp) — that link is made on Facebook, not here, and needs no extra Hermoso permission. The shop choice is sent on the ad CREATIVE (destination_spec) — "WEBSITE" or "WEBSITE_AND_SHOP_OPT_OUT" switches the in-app Shop OFF, "WEBSITE_AND_SHOP" switches it on — and the reply reads back what Meta stored. |
| instagramUserId | string | run it on Instagram under the brand’s own handle |
| playableAssetId | string | a PLAYABLE from upload_meta_playable, shown after the lead-in video. Needs adSetId of an App Installs ad set and exactly ONE video; the button installs the app the ad set promotes |
| optimizationGoal | string | override, e.g. OFFSITE_CONVERSIONS / LANDING_PAGE_VIEWS / THRUPLAY / VALUE |
| lifetimeBudgetUsd | number | a fixed total instead of a daily budget, in the ad account's currency — REQUIRES endTime |
| customConversionId | string | Meta custom conversion id (digits), from Meta Events Manager |
| specialAdCategories | array | legally required when the ad falls in one of these categories — it restricts targeting |
| whatsappPhoneNumber | string | CLICK-TO-WHATSAPP: the WhatsApp Business number the chat opens with, in international format. Sent as the ad set’s promoted whatsapp_phone_number when destinationType is WHATSAPP; Meta still requires that number to be linked to the Page. |
| creativeEnhancements | any | META'S AUTOMATIC CREATIVE ENHANCEMENTS (Advantage+ creative: AI text overlays, stickers, templates, generated backgrounds, uncropping, text rewrites, translation…). DEFAULT "off": every one is explicitly switched OFF so the ad runs exactly as made. "meta" leaves Meta's own defaults. Or an object turning named ones on, e.g. {"image_touchups": true} (everything else stays off). Names: adapt_to_placement, add_text_overlay, creative_stickers, description_automation, enhance_cta, image_animation, image_background_gen, image_brightness_and_contrast, image_templates, image_text_translation, image_touchups, image_uncrop, inline_comment, media_type_automation, pac_relaxation, product_extensions, reveal_details_over_time, text_optimizations, text_translation, translate_voiceover, video_auto_crop, video_filtering, video_uncrop. The reply reads back what Meta stored. |
| brandedContentSponsorIgId | string | PARTNERSHIP AD — the Instagram account id shown as the PAID PARTNER on this ad (Meta’s `instagram_branded_content.sponsor_id`), so the ad runs as a creator collaboration rather than as a plain brand ad. Hermoso sends this id EXACTLY as given and does not work out which id belongs in which slot — Meta’s own two published examples disagree on that — so confirm the ad’s identity in Ads Manager before it is switched on. |
| brandedContentSponsorPageId | string | PARTNERSHIP AD — the Facebook Page id of the paid partner (Meta’s `facebook_branded_content.sponsor_page_id`). Give BOTH halves when the ad runs across both surfaces: supplying only one leaves the other surface’s placements with no partnership label at all, and the returned summary says which half is missing rather than letting it be discovered in Ads Manager. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"cta": {
"type": "string",
"description": "call-to-action, e.g. SHOP_NOW / LEARN_MORE / SIGN_UP (default LEARN_MORE); WHATSAPP_MESSAGE for a click-to-WhatsApp ad (see destinationType). Any Meta call-to-action type works (SEE_DETAILS…); unknown ones are refused, not swapped."
},
"link": {
"type": "string",
"description": "destination URL (defaults to the brand domain)"
},
"name": {
"type": "string",
"description": "base name for the campaign/ad set/ads"
},
"cards": {
"type": "array",
"items": {
"type": "object",
"properties": {
"link": {
"type": "string",
"description": "destination URL for this carousel card"
},
"headline": {
"type": "string",
"description": "headline for this carousel card"
},
"description": {
"type": "string",
"description": "description line for this carousel card"
}
}
},
"description": "carousel cards in order — each may set its own headline/description/link"
},
"format": {
"enum": [
"auto",
"carousel"
],
"type": "string",
"description": "auto = one ad per asset (image or video); carousel = ONE multi-card ad"
},
"pageId": {
"type": "string",
"description": "Omit when the brand has one Page; with several the call lists them, so ask the user which one."
},
"adSetId": {
"type": "string",
"description": "attach the ad(s) to an EXISTING ad set (skips ad-set creation)"
},
"country": {
"type": "string",
"description": "2-letter shorthand when you are not passing full targeting (default US)"
},
"endTime": {
"type": "string",
"description": "REQUIRED with lifetimeBudgetUsd"
},
"message": {
"type": "string",
"description": "primary ad text"
},
"minRoas": {
"type": "number",
"description": "REQUIRED for LOWEST_COST_WITH_MIN_ROAS, e.g. 1.1"
},
"pixelId": {
"type": "string",
"description": "Meta Pixel id — with this the ad set optimizes for a real CONVERSION instead of falling back to link clicks"
},
"urlTags": {
"type": [
"string",
"boolean"
],
"description": "UTM click tracking Meta appends to every click (the creative's url_tags). DEFAULT ON: utm_source=meta&utm_medium=paid&utm_campaign={{campaign.name}}&utm_term={{adset.name}}&utm_content={{ad.name}} (Meta fills the {{…}} per click) — skipped automatically when your link already has utm_ parameters. Pass your own query string to use it exactly as written, or false to send none. Not used on lead-form or click-to-message ads. The reply reads back what Meta stored."
},
"dsaPayor": {
"type": "string",
"description": "EU AD TRANSPARENCY: who pays for the ads (Meta shows it on the ad). Omit it and it matches dsaBeneficiary"
},
"headline": {
"type": "string",
"description": "headline"
},
"imageUrl": {
"type": "string",
"description": "public https image URL for the ad creative"
},
"videoUrl": {
"type": "string",
"description": "a video URL -> a real Meta VIDEO ad (uploaded + transcoded + thumbnailed for you)"
},
"imageUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "several image URLs -> one ad each, or the carousel cards in order"
},
"objective": {
"enum": [
"OUTCOME_TRAFFIC",
"OUTCOME_AWARENESS",
"OUTCOME_ENGAGEMENT",
"OUTCOME_LEADS",
"OUTCOME_SALES"
],
"type": "string",
"description": "default OUTCOME_TRAFFIC"
},
"startTime": {
"type": "string",
"description": "ISO-8601 with offset, e.g. 2026-08-01T09:00:00-0700"
},
"targeting": {
"type": "object",
"properties": {
"geo": {
"type": "object",
"properties": {
"zips": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta postal code key (the ads location search returns it)"
}
}
},
"description": "postal codes to target: list of {key}"
},
"cities": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric city key, from find_meta_audiences"
},
"radius": {
"type": "number",
"description": "radius around the city: 10 to 50 miles or 17 to 80 kilometers"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "city KEYS; radius works here (10–50 mi / 17–80 km)"
},
"regions": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric region key (the ads location search returns it)"
}
}
},
"description": "region KEYS from find_meta_audiences(type:\"adgeolocation\")"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-letter codes, e.g. [\"US\",\"CA\"]"
},
"geoMarkets": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta geo market (DMA) key (the ads location search returns it)"
}
}
},
"description": "DMA keys, e.g. {key:\"DMA:807\"}"
},
"locationTypes": {
"type": "array",
"items": {
"enum": [
"home",
"recent",
"travel_in"
],
"type": "string"
},
"description": "people who LIVE there vs were recently there"
},
"customLocations": {
"type": "array",
"items": {
"type": "object",
"required": [
"latitude",
"longitude"
],
"properties": {
"radius": {
"type": "number",
"description": "radius around the pin: 0.63 to 50 miles or 1 to 80 kilometers"
},
"latitude": {
"type": "number",
"description": "latitude of a custom pin location"
},
"longitude": {
"type": "number",
"description": "longitude of a custom pin location"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "drop a pin + radius"
}
},
"description": "where the ad runs",
"additionalProperties": {}
},
"ageMax": {
"type": "number",
"description": "13–65 (65 means 65+)"
},
"ageMin": {
"type": "number",
"description": "13–65"
},
"userOs": {
"type": "array",
"items": {
"enum": [
"iOS",
"Android"
],
"type": "string"
},
"description": "iOS or Android"
},
"genders": {
"enum": [
"all",
"men",
"women"
],
"type": "string",
"description": "all, men or women"
},
"locales": {
"type": "array",
"items": {
"type": "number"
},
"description": "Meta language ids from find_meta_audiences(type:\"adlocale\")"
},
"behaviors": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "behaviour ids from find_meta_audiences(type:\"adTargetingCategory\", class:\"behaviors\")"
},
"interests": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "interest ids from find_meta_audiences(type:\"adinterest\")"
},
"excludedGeo": {
"type": "object",
"properties": {
"zips": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta postal code key (the ads location search returns it)"
}
}
},
"description": "postal codes to target: list of {key}"
},
"cities": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric city key, from find_meta_audiences"
},
"radius": {
"type": "number",
"description": "radius around the city: 10 to 50 miles or 17 to 80 kilometers"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "city KEYS; radius works here (10–50 mi / 17–80 km)"
},
"regions": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric region key (the ads location search returns it)"
}
}
},
"description": "region KEYS from find_meta_audiences(type:\"adgeolocation\")"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-letter codes, e.g. [\"US\",\"CA\"]"
},
"geoMarkets": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta geo market (DMA) key (the ads location search returns it)"
}
}
},
"description": "DMA keys, e.g. {key:\"DMA:807\"}"
},
"locationTypes": {
"type": "array",
"items": {
"enum": [
"home",
"recent",
"travel_in"
],
"type": "string"
},
"description": "people who LIVE there vs were recently there"
},
"customLocations": {
"type": "array",
"items": {
"type": "object",
"required": [
"latitude",
"longitude"
],
"properties": {
"radius": {
"type": "number",
"description": "radius around the pin: 0.63 to 50 miles or 1 to 80 kilometers"
},
"latitude": {
"type": "number",
"description": "latitude of a custom pin location"
},
"longitude": {
"type": "number",
"description": "longitude of a custom pin location"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "drop a pin + radius"
}
},
"description": "places to exclude",
"additionalProperties": {}
},
"flexibleSpec": {
"type": "array",
"items": {
"type": "object",
"properties": {
"behaviors": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "behaviors in this group, matched as OR within the group: list of {id, name}"
},
"interests": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "interests in this group, matched as OR within the group: list of {id, name}"
}
}
},
"description": "AND across entries, OR within one"
},
"customAudiences": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "saved audiences AND lookalikes — a lookalike IS a custom audience id"
},
"deviceExpansion": {
"type": "boolean",
"description": "only with devicePlatforms / userOs: true lets Meta also show the ad on OTHER devices and operating systems (its \"Reach more people across other mobile devices\", Sales campaigns only). Default false keeps delivery to exactly the devices and OS you targeted — Hermoso sends the opt-out explicitly, because Meta turns this on by default for new Sales ad sets from Graph v27"
},
"devicePlatforms": {
"type": "array",
"items": {
"enum": [
"mobile",
"desktop"
],
"type": "string"
},
"description": "mobile or desktop"
},
"advantageAudience": {
"type": "boolean",
"description": "let Meta expand beyond your audience (Advantage+ audience)"
},
"excludedBehaviors": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "behaviors to exclude: list of {id, name}"
},
"excludedInterests": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "interests to exclude: list of {id, name}"
},
"facebookPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "feed, story, facebook_reels, marketplace, search, instream_video, right_hand_column, …"
},
"instagramPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "stream, story, reels, explore, profile_feed, …"
},
"messengerPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Messenger placements, e.g. sponsored_messages"
},
"publisherPlatforms": {
"type": "array",
"items": {
"enum": [
"facebook",
"instagram",
"audience_network",
"messenger",
"threads"
],
"type": "string"
},
"description": "placement platforms: facebook, instagram, audience_network, messenger or threads"
},
"excludedCustomAudiences": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "custom audiences to exclude: list of {id, name}"
},
"audienceNetworkPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Audience Network placement names, as Meta defines them"
}
},
"description": "full Meta ad-set targeting — age, gender, geo, interests, behaviours, audiences, languages, placements, devices. Use EXACTLY these key names: an unrecognised one (e.g. geoLocations) is REFUSED by name — it is never dropped, because a dropped geo key used to fall back to targeting the United States.",
"additionalProperties": {}
},
"campaignId": {
"type": "string",
"description": "attach to an existing campaign instead of creating one"
},
"leadFormId": {
"type": "string",
"description": "LEAD ADS — attach an instant form (from list_meta_lead_forms / create_meta_lead_form) so the button opens that form INSIDE Facebook/Instagram instead of sending the click to a website. Requires objective:\"OUTCOME_LEADS\"; the ad set is then optimized for LEAD_GENERATION and pointed at the form automatically. Read the submissions afterwards with read_meta_leads."
},
"adAccountId": {
"type": "string",
"description": "Ad account id"
},
"bidStrategy": {
"enum": [
"LOWEST_COST_WITHOUT_CAP",
"LOWEST_COST_WITH_BID_CAP",
"COST_CAP",
"LOWEST_COST_WITH_MIN_ROAS"
],
"type": "string",
"description": "bidding: lowest cost, bid cap, cost cap or minimum ROAS (Meta's strategy names)"
},
"boostPostId": {
"type": "string",
"description": "Promote a post that ALREADY EXISTS instead of building a new ad from media. Accepts the numeric post id, <pageId>_<postId>, or a permalink (an Instagram post is its NUMERIC media id — an instagram.com link carries only a shortcode, which Meta cannot resolve). Cannot be combined with image/video inputs, and creative fields do not apply — a boost shows the post as published."
},
"boostTarget": {
"enum": [
"facebook",
"instagram"
],
"type": "string",
"description": "Which surface the boosted post lives on. Default facebook. REQUIRED for an Instagram post: an IG media id and a Facebook post id are both bare digits, so this is never inferred — Meta takes a different creative for each (object_story_id for a Page post; object_id + instagram_user_id + source_instagram_media_id for an IG post)."
},
"description": {
"type": "string",
"description": "the smaller description line under the headline"
},
"bidAmountUsd": {
"type": "number",
"description": "REQUIRED for a bid cap / cost cap"
},
"billingEvent": {
"type": "string",
"description": "default IMPRESSIONS"
},
"productSetId": {
"type": "string",
"description": "CATALOG (dynamic product) ADS. Meta binds a catalog at TWO levels and they are different ids on different objects: the CAMPAIGN takes productCatalogId, the AD SET takes productSetId. Passing a CATALOG id here is refused by name rather than moved, because Meta’s own error for a mis-placed id names neither the level nor the right field. Cannot be combined with pixelId — an ad set has exactly one promoted_object — and its conversion event is conversionEvent, which Meta defaults to PURCHASE."
},
"thumbnailUrl": {
"type": "string",
"description": "custom video thumbnail (otherwise Meta picks a frame)"
},
"adsetSchedule": {
"type": "array",
"items": {
"type": "object",
"required": [
"startMinute",
"endMinute",
"days"
],
"properties": {
"days": {
"type": "array",
"items": {
"type": "number"
},
"description": "weekdays the window runs on: 0 = Sunday through 6 = Saturday"
},
"endMinute": {
"type": "number",
"description": "end of the daypart window, minutes from midnight (0 to 1440)"
},
"startMinute": {
"type": "number",
"description": "start of the daypart window, minutes from midnight (0 to 1440)"
}
}
},
"description": "dayparting — minutes from midnight (0–1440), days 0=Sunday…6=Saturday"
},
"applicationId": {
"type": "string",
"description": "app-promotion ads"
},
"dailyBudgetUsd": {
"type": "number",
"description": "ad-set daily budget in the AD ACCOUNT's currency (historical name — not necessarily USD; default 10, or 1,000 in a whole-unit currency like JPY) — spends only once ACTIVE. Under a campaign that HAS its own budget (create_meta_campaign with dailyBudgetUsd, Advantage+ campaign budget) leave this and lifetimeBudgetUsd OUT: Meta allows a budget at the campaign or the ad set, never both, so the ad set then gets none and shares the campaign’s, and naming one is refused by name before anything is created"
},
"dsaBeneficiary": {
"type": "string",
"description": "EU AD TRANSPARENCY: the person or organisation this ad promotes (Meta shows it on the ad). Meta requires it whenever the ad set reaches the EU; omit it and Hermoso uses the ad account's own default, else this profile's brand name, else the Page name, and the summary says which was used"
},
"objectStoreUrl": {
"type": "string",
"description": "app store link for an app-install ad"
},
"attributionSpec": {
"type": "array",
"items": {},
"description": "e.g. [{event_type:\"CLICK_THROUGH\",window_days:7}]"
},
"carouselEndCard": {
"type": "boolean",
"description": "append the Page end card to a carousel"
},
"conversionEvent": {
"type": "string",
"description": "PURCHASE | LEAD | COMPLETE_REGISTRATION | ADD_TO_CART | INITIATED_CHECKOUT | …"
},
"destinationType": {
"type": "string",
"description": "WHERE A CLICK LANDS, and worth deciding for a merchant with a shop. \"WEBSITE\" sends it to their own site — their pixel fires, their email capture runs, their upsell flow works. \"WEBSITE_AND_SHOP\" lets Meta route it into the in-app Facebook/Instagram Shop instead: often fewer taps to a purchase, but the visit never reaches their site. FROM META API v26 AN ADVERTISER WITH A SHOP DEFAULTS TO WEBSITE_AND_SHOP, so pass \"WEBSITE_AND_SHOP_OPT_OUT\" to keep every click on their own site. Also accepts APP, MESSENGER, INSTAGRAM_DIRECT, WHATSAPP, ON_AD, ON_POST, ON_PAGE, ON_EVENT, ON_VIDEO, SHOP_AUTOMATIC. An unknown value is refused by name, and the read-back says in words where clicks will go. CLICK-TO-WHATSAPP ADS (certified against Meta 2026-09-03): pass destinationType \"WHATSAPP\", cta \"WHATSAPP_MESSAGE\" and optimizationGoal \"CONVERSATIONS\" (objective OUTCOME_ENGAGEMENT). Meta refuses the ad set unless the Page already has a WhatsApp number linked (Facebook Page settings > WhatsApp) — that link is made on Facebook, not here, and needs no extra Hermoso permission. The shop choice is sent on the ad CREATIVE (destination_spec) — \"WEBSITE\" or \"WEBSITE_AND_SHOP_OPT_OUT\" switches the in-app Shop OFF, \"WEBSITE_AND_SHOP\" switches it on — and the reply reads back what Meta stored."
},
"instagramUserId": {
"type": "string",
"description": "run it on Instagram under the brand’s own handle"
},
"playableAssetId": {
"type": "string",
"description": "a PLAYABLE from upload_meta_playable, shown after the lead-in video. Needs adSetId of an App Installs ad set and exactly ONE video; the button installs the app the ad set promotes"
},
"optimizationGoal": {
"type": "string",
"description": "override, e.g. OFFSITE_CONVERSIONS / LANDING_PAGE_VIEWS / THRUPLAY / VALUE"
},
"lifetimeBudgetUsd": {
"type": "number",
"description": "a fixed total instead of a daily budget, in the ad account's currency — REQUIRES endTime"
},
"customConversionId": {
"type": "string",
"description": "Meta custom conversion id (digits), from Meta Events Manager"
},
"specialAdCategories": {
"type": "array",
"items": {
"enum": [
"HOUSING",
"EMPLOYMENT",
"CREDIT",
"ISSUES_ELECTIONS_POLITICS",
"ONLINE_GAMBLING_AND_GAMING",
"FINANCIAL_PRODUCTS_SERVICES"
],
"type": "string"
},
"description": "legally required when the ad falls in one of these categories — it restricts targeting"
},
"whatsappPhoneNumber": {
"type": "string",
"description": "CLICK-TO-WHATSAPP: the WhatsApp Business number the chat opens with, in international format. Sent as the ad set’s promoted whatsapp_phone_number when destinationType is WHATSAPP; Meta still requires that number to be linked to the Page."
},
"creativeEnhancements": {
"anyOf": [
{
"enum": [
"off",
"meta"
],
"type": "string"
},
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "boolean"
}
}
],
"description": "META'S AUTOMATIC CREATIVE ENHANCEMENTS (Advantage+ creative: AI text overlays, stickers, templates, generated backgrounds, uncropping, text rewrites, translation…). DEFAULT \"off\": every one is explicitly switched OFF so the ad runs exactly as made. \"meta\" leaves Meta's own defaults. Or an object turning named ones on, e.g. {\"image_touchups\": true} (everything else stays off). Names: adapt_to_placement, add_text_overlay, creative_stickers, description_automation, enhance_cta, image_animation, image_background_gen, image_brightness_and_contrast, image_templates, image_text_translation, image_touchups, image_uncrop, inline_comment, media_type_automation, pac_relaxation, product_extensions, reveal_details_over_time, text_optimizations, text_translation, translate_voiceover, video_auto_crop, video_filtering, video_uncrop. The reply reads back what Meta stored."
},
"brandedContentSponsorIgId": {
"type": "string",
"description": "PARTNERSHIP AD — the Instagram account id shown as the PAID PARTNER on this ad (Meta’s `instagram_branded_content.sponsor_id`), so the ad runs as a creator collaboration rather than as a plain brand ad. Hermoso sends this id EXACTLY as given and does not work out which id belongs in which slot — Meta’s own two published examples disagree on that — so confirm the ad’s identity in Ads Manager before it is switched on."
},
"brandedContentSponsorPageId": {
"type": "string",
"description": "PARTNERSHIP AD — the Facebook Page id of the paid partner (Meta’s `facebook_branded_content.sponsor_page_id`). Give BOTH halves when the ad runs across both surfaces: supplying only one leaves the other surface’s placements with no partnership label at all, and the returned summary says which half is missing rather than letting it be discovered in Ads Manager."
}
}
}| EU AD TRANSPARENCY: who pays for the ads (Meta shows it on the ad). Omit it and it matches dsaBeneficiary |
| startTime | string | ISO-8601 with offset, e.g. 2026-08-01T09:00:00-0700 |
| targeting | object | full Meta ad-set targeting — age, gender, geo, interests, behaviours, audiences, languages, placements, devices. Use EXACTLY these key names: an unrecognised one (e.g. geoLocations) is REFUSED by name — it is never dropped, because a dropped geo key used to fall back to targeting the United States. |
| campaignId* | string | the campaign this ad set belongs to |
| adAccountId* | string | ad account id (act_… or digits) |
| bidStrategy | string | bidding: lowest cost, bid cap, cost cap or minimum ROAS (Meta's strategy names) |
| bidAmountUsd | number | REQUIRED for a bid cap / cost cap |
| billingEvent | string | default IMPRESSIONS |
| productSetId | string | CATALOG (dynamic product) ADS. Meta binds a catalog at TWO levels and they are different ids on different objects: the CAMPAIGN takes productCatalogId, the AD SET takes productSetId. Passing a CATALOG id here is refused by name rather than moved, because Meta’s own error for a mis-placed id names neither the level nor the right field. Cannot be combined with pixelId — an ad set has exactly one promoted_object — and its conversion event is conversionEvent, which Meta defaults to PURCHASE. |
| adsetSchedule | array | dayparting — minutes from midnight (0–1440), days 0=Sunday…6=Saturday |
| applicationId | string | app-promotion ads |
| dailyBudgetUsd | number | ad-set daily budget in the AD ACCOUNT's currency (historical name — not necessarily USD; default 10, or 1,000 in a whole-unit currency like JPY) — spends only once ACTIVE. Under a campaign that HAS its own budget (create_meta_campaign with dailyBudgetUsd, Advantage+ campaign budget) leave this and lifetimeBudgetUsd OUT: Meta allows a budget at the campaign or the ad set, never both, so the ad set then gets none and shares the campaign’s, and naming one is refused by name before anything is created |
| dsaBeneficiary | string | EU AD TRANSPARENCY: the person or organisation this ad promotes (Meta shows it on the ad). Meta requires it whenever the ad set reaches the EU; omit it and Hermoso uses the ad account's own default, else this profile's brand name, else the Page name, and the summary says which was used |
| objectStoreUrl | string | app store link for an app-install ad |
| attributionSpec | array | e.g. [{event_type:"CLICK_THROUGH",window_days:7}] |
| conversionEvent | string | PURCHASE | LEAD | COMPLETE_REGISTRATION | ADD_TO_CART | INITIATED_CHECKOUT | … |
| optimizationGoal | string | override, e.g. OFFSITE_CONVERSIONS / LANDING_PAGE_VIEWS / THRUPLAY / VALUE |
| lifetimeBudgetUsd | number | a fixed total instead of a daily budget, in the ad account's currency — REQUIRES endTime |
| customConversionId | string | Meta custom conversion id (digits), from Meta Events Manager |
| brandedContentSponsorIgId | string | PARTNERSHIP AD — the Instagram account id shown as the PAID PARTNER on this ad (Meta’s `instagram_branded_content.sponsor_id`), so the ad runs as a creator collaboration rather than as a plain brand ad. Hermoso sends this id EXACTLY as given and does not work out which id belongs in which slot — Meta’s own two published examples disagree on that — so confirm the ad’s identity in Ads Manager before it is switched on. |
| brandedContentSponsorPageId | string | PARTNERSHIP AD — the Facebook Page id of the paid partner (Meta’s `facebook_branded_content.sponsor_page_id`). Give BOTH halves when the ad runs across both surfaces: supplying only one leaves the other surface’s placements with no partnership label at all, and the returned summary says which half is missing rather than letting it be discovered in Ads Manager. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId",
"campaignId"
],
"properties": {
"name": {
"type": "string",
"description": "ad set name"
},
"pageId": {
"type": "string",
"description": "Omit when the brand has one Page; with several the call lists them, so ask the user which one."
},
"country": {
"type": "string",
"description": "2-letter shorthand when you are not passing full targeting (default US)"
},
"endTime": {
"type": "string",
"description": "REQUIRED with lifetimeBudgetUsd"
},
"minRoas": {
"type": "number",
"description": "REQUIRED for LOWEST_COST_WITH_MIN_ROAS, e.g. 1.1"
},
"pixelId": {
"type": "string",
"description": "Meta Pixel id — with this the ad set optimizes for a real CONVERSION instead of falling back to link clicks"
},
"dsaPayor": {
"type": "string",
"description": "EU AD TRANSPARENCY: who pays for the ads (Meta shows it on the ad). Omit it and it matches dsaBeneficiary"
},
"startTime": {
"type": "string",
"description": "ISO-8601 with offset, e.g. 2026-08-01T09:00:00-0700"
},
"targeting": {
"type": "object",
"properties": {
"geo": {
"type": "object",
"properties": {
"zips": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta postal code key (the ads location search returns it)"
}
}
},
"description": "postal codes to target: list of {key}"
},
"cities": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric city key, from find_meta_audiences"
},
"radius": {
"type": "number",
"description": "radius around the city: 10 to 50 miles or 17 to 80 kilometers"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "city KEYS; radius works here (10–50 mi / 17–80 km)"
},
"regions": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric region key (the ads location search returns it)"
}
}
},
"description": "region KEYS from find_meta_audiences(type:\"adgeolocation\")"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-letter codes, e.g. [\"US\",\"CA\"]"
},
"geoMarkets": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta geo market (DMA) key (the ads location search returns it)"
}
}
},
"description": "DMA keys, e.g. {key:\"DMA:807\"}"
},
"locationTypes": {
"type": "array",
"items": {
"enum": [
"home",
"recent",
"travel_in"
],
"type": "string"
},
"description": "people who LIVE there vs were recently there"
},
"customLocations": {
"type": "array",
"items": {
"type": "object",
"required": [
"latitude",
"longitude"
],
"properties": {
"radius": {
"type": "number",
"description": "radius around the pin: 0.63 to 50 miles or 1 to 80 kilometers"
},
"latitude": {
"type": "number",
"description": "latitude of a custom pin location"
},
"longitude": {
"type": "number",
"description": "longitude of a custom pin location"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "drop a pin + radius"
}
},
"description": "where the ad runs",
"additionalProperties": {}
},
"ageMax": {
"type": "number",
"description": "13–65 (65 means 65+)"
},
"ageMin": {
"type": "number",
"description": "13–65"
},
"userOs": {
"type": "array",
"items": {
"enum": [
"iOS",
"Android"
],
"type": "string"
},
"description": "iOS or Android"
},
"genders": {
"enum": [
"all",
"men",
"women"
],
"type": "string",
"description": "all, men or women"
},
"locales": {
"type": "array",
"items": {
"type": "number"
},
"description": "Meta language ids from find_meta_audiences(type:\"adlocale\")"
},
"behaviors": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "behaviour ids from find_meta_audiences(type:\"adTargetingCategory\", class:\"behaviors\")"
},
"interests": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "interest ids from find_meta_audiences(type:\"adinterest\")"
},
"excludedGeo": {
"type": "object",
"properties": {
"zips": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta postal code key (the ads location search returns it)"
}
}
},
"description": "postal codes to target: list of {key}"
},
"cities": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric city key, from find_meta_audiences"
},
"radius": {
"type": "number",
"description": "radius around the city: 10 to 50 miles or 17 to 80 kilometers"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "city KEYS; radius works here (10–50 mi / 17–80 km)"
},
"regions": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta numeric region key (the ads location search returns it)"
}
}
},
"description": "region KEYS from find_meta_audiences(type:\"adgeolocation\")"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-letter codes, e.g. [\"US\",\"CA\"]"
},
"geoMarkets": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string",
"description": "Meta geo market (DMA) key (the ads location search returns it)"
}
}
},
"description": "DMA keys, e.g. {key:\"DMA:807\"}"
},
"locationTypes": {
"type": "array",
"items": {
"enum": [
"home",
"recent",
"travel_in"
],
"type": "string"
},
"description": "people who LIVE there vs were recently there"
},
"customLocations": {
"type": "array",
"items": {
"type": "object",
"required": [
"latitude",
"longitude"
],
"properties": {
"radius": {
"type": "number",
"description": "radius around the pin: 0.63 to 50 miles or 1 to 80 kilometers"
},
"latitude": {
"type": "number",
"description": "latitude of a custom pin location"
},
"longitude": {
"type": "number",
"description": "longitude of a custom pin location"
},
"distanceUnit": {
"enum": [
"mile",
"kilometer"
],
"type": "string",
"description": "mile or kilometer; the unit the radius is measured in"
}
}
},
"description": "drop a pin + radius"
}
},
"description": "places to exclude",
"additionalProperties": {}
},
"flexibleSpec": {
"type": "array",
"items": {
"type": "object",
"properties": {
"behaviors": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "behaviors in this group, matched as OR within the group: list of {id, name}"
},
"interests": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "interests in this group, matched as OR within the group: list of {id, name}"
}
}
},
"description": "AND across entries, OR within one"
},
"customAudiences": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "saved audiences AND lookalikes — a lookalike IS a custom audience id"
},
"deviceExpansion": {
"type": "boolean",
"description": "only with devicePlatforms / userOs: true lets Meta also show the ad on OTHER devices and operating systems (its \"Reach more people across other mobile devices\", Sales campaigns only). Default false keeps delivery to exactly the devices and OS you targeted — Hermoso sends the opt-out explicitly, because Meta turns this on by default for new Sales ad sets from Graph v27"
},
"devicePlatforms": {
"type": "array",
"items": {
"enum": [
"mobile",
"desktop"
],
"type": "string"
},
"description": "mobile or desktop"
},
"advantageAudience": {
"type": "boolean",
"description": "let Meta expand beyond your audience (Advantage+ audience)"
},
"excludedBehaviors": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "behaviors to exclude: list of {id, name}"
},
"excludedInterests": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "interests to exclude: list of {id, name}"
},
"facebookPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "feed, story, facebook_reels, marketplace, search, instream_video, right_hand_column, …"
},
"instagramPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "stream, story, reels, explore, profile_feed, …"
},
"messengerPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Messenger placements, e.g. sponsored_messages"
},
"publisherPlatforms": {
"type": "array",
"items": {
"enum": [
"facebook",
"instagram",
"audience_network",
"messenger",
"threads"
],
"type": "string"
},
"description": "placement platforms: facebook, instagram, audience_network, messenger or threads"
},
"excludedCustomAudiences": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "Meta interest id, from find_meta_audiences"
},
"name": {
"type": "string",
"description": "interest name, for readability; the id is what targets"
}
}
},
"description": "custom audiences to exclude: list of {id, name}"
},
"audienceNetworkPositions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Audience Network placement names, as Meta defines them"
}
},
"description": "full Meta ad-set targeting — age, gender, geo, interests, behaviours, audiences, languages, placements, devices. Use EXACTLY these key names: an unrecognised one (e.g. geoLocations) is REFUSED by name — it is never dropped, because a dropped geo key used to fall back to targeting the United States.",
"additionalProperties": {}
},
"campaignId": {
"type": "string",
"description": "the campaign this ad set belongs to"
},
"adAccountId": {
"type": "string",
"description": "ad account id (act_… or digits)"
},
"bidStrategy": {
"enum": [
"LOWEST_COST_WITHOUT_CAP",
"LOWEST_COST_WITH_BID_CAP",
"COST_CAP",
"LOWEST_COST_WITH_MIN_ROAS"
],
"type": "string",
"description": "bidding: lowest cost, bid cap, cost cap or minimum ROAS (Meta's strategy names)"
},
"bidAmountUsd": {
"type": "number",
"description": "REQUIRED for a bid cap / cost cap"
},
"billingEvent": {
"type": "string",
"description": "default IMPRESSIONS"
},
"productSetId": {
"type": "string",
"description": "CATALOG (dynamic product) ADS. Meta binds a catalog at TWO levels and they are different ids on different objects: the CAMPAIGN takes productCatalogId, the AD SET takes productSetId. Passing a CATALOG id here is refused by name rather than moved, because Meta’s own error for a mis-placed id names neither the level nor the right field. Cannot be combined with pixelId — an ad set has exactly one promoted_object — and its conversion event is conversionEvent, which Meta defaults to PURCHASE."
},
"adsetSchedule": {
"type": "array",
"items": {
"type": "object",
"required": [
"startMinute",
"endMinute",
"days"
],
"properties": {
"days": {
"type": "array",
"items": {
"type": "number"
},
"description": "weekdays the window runs on: 0 = Sunday through 6 = Saturday"
},
"endMinute": {
"type": "number",
"description": "end of the daypart window, minutes from midnight (0 to 1440)"
},
"startMinute": {
"type": "number",
"description": "start of the daypart window, minutes from midnight (0 to 1440)"
}
}
},
"description": "dayparting — minutes from midnight (0–1440), days 0=Sunday…6=Saturday"
},
"applicationId": {
"type": "string",
"description": "app-promotion ads"
},
"dailyBudgetUsd": {
"type": "number",
"description": "ad-set daily budget in the AD ACCOUNT's currency (historical name — not necessarily USD; default 10, or 1,000 in a whole-unit currency like JPY) — spends only once ACTIVE. Under a campaign that HAS its own budget (create_meta_campaign with dailyBudgetUsd, Advantage+ campaign budget) leave this and lifetimeBudgetUsd OUT: Meta allows a budget at the campaign or the ad set, never both, so the ad set then gets none and shares the campaign’s, and naming one is refused by name before anything is created"
},
"dsaBeneficiary": {
"type": "string",
"description": "EU AD TRANSPARENCY: the person or organisation this ad promotes (Meta shows it on the ad). Meta requires it whenever the ad set reaches the EU; omit it and Hermoso uses the ad account's own default, else this profile's brand name, else the Page name, and the summary says which was used"
},
"objectStoreUrl": {
"type": "string",
"description": "app store link for an app-install ad"
},
"attributionSpec": {
"type": "array",
"items": {},
"description": "e.g. [{event_type:\"CLICK_THROUGH\",window_days:7}]"
},
"conversionEvent": {
"type": "string",
"description": "PURCHASE | LEAD | COMPLETE_REGISTRATION | ADD_TO_CART | INITIATED_CHECKOUT | …"
},
"optimizationGoal": {
"type": "string",
"description": "override, e.g. OFFSITE_CONVERSIONS / LANDING_PAGE_VIEWS / THRUPLAY / VALUE"
},
"lifetimeBudgetUsd": {
"type": "number",
"description": "a fixed total instead of a daily budget, in the ad account's currency — REQUIRES endTime"
},
"customConversionId": {
"type": "string",
"description": "Meta custom conversion id (digits), from Meta Events Manager"
},
"brandedContentSponsorIgId": {
"type": "string",
"description": "PARTNERSHIP AD — the Instagram account id shown as the PAID PARTNER on this ad (Meta’s `instagram_branded_content.sponsor_id`), so the ad runs as a creator collaboration rather than as a plain brand ad. Hermoso sends this id EXACTLY as given and does not work out which id belongs in which slot — Meta’s own two published examples disagree on that — so confirm the ad’s identity in Ads Manager before it is switched on."
},
"brandedContentSponsorPageId": {
"type": "string",
"description": "PARTNERSHIP AD — the Facebook Page id of the paid partner (Meta’s `facebook_branded_content.sponsor_page_id`). Give BOTH halves when the ad runs across both surfaces: supplying only one leaves the other surface’s placements with no partnership label at all, and the returned summary says which half is missing rather than letting it be discovered in Ads Manager."
}
}
}| string |
| audience name (required unless adding rows to an existing list with audienceId) |
| rows | array | customerfile: the customers, one array per person in schema order (up to 10,000 per call). Plain values — Hermoso normalizes and hashes them before they leave the server |
| event | string | engagement: page_engaged | page_visited | page_liked | page_messaged | page_cta_clicked | page_or_post_save | page_post_interaction — or ig_business_profile_all | ig_business_profile_engaged | ig_user_messaged_business | ig_business_profile_visit |
| ratio | number | lookalike: 0.01–0.20 = the top 1%–20% most similar people (default 0.01) |
| pageId | string | engagement: which connected Page (required only if the profile has several) |
| schema | array | customerfile: the column names of rows, in order, e.g. ["EMAIL","PHONE"] |
| source | string | engagement: Facebook Page (default) or the linked Instagram business profile |
| country | string | lookalike: REQUIRED 2-letter country you will run it in — Meta delivers a lookalike to the locations of the ad set that targets it, so target this country there |
| pixelId | string | website: the Meta Pixel that sees the traffic |
| prefill | boolean | website/engagement: seed it with activity from BEFORE the audience existed (default true) |
| audienceId | string | customerfile: ADD these rows to this existing customer-list audience (from list_meta_audiences) instead of creating a new one |
| adAccountId* | string | ad account id (act_… or digits) |
| description | string | optional free-text note stored with the audience |
| urlContains | string | website: the URL fragment that defines the audience (your domain = all visitors) |
| lookalikeType | string | lookalike: similarity (tighter) or reach (broader) — default similarity |
| retentionDays | number | how long someone stays in it — website max 180, engagement max 730 (default 30) |
| startingRatio | number | lookalike: optional lower bound for a band (e.g. 0.01 with ratio 0.03 = the 1%–3% band) — at least 0.01 and less than ratio; not combinable with lookalikeType |
| sourceAudienceId | string | lookalike: the existing audience to model (from list_meta_audiences) |
| customerFileSource | string | customerfile: REQUIRED attestation of where the list came from (customers gave it to you = USER_PROVIDED_ONLY) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId",
"kind"
],
"properties": {
"kind": {
"enum": [
"website",
"engagement",
"lookalike",
"customerfile"
],
"type": "string",
"description": "which kind of audience to build — customerfile = your own customer list"
},
"name": {
"type": "string",
"description": "audience name (required unless adding rows to an existing list with audienceId)"
},
"rows": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number"
]
}
},
"description": "customerfile: the customers, one array per person in schema order (up to 10,000 per call). Plain values — Hermoso normalizes and hashes them before they leave the server"
},
"event": {
"type": "string",
"description": "engagement: page_engaged | page_visited | page_liked | page_messaged | page_cta_clicked | page_or_post_save | page_post_interaction — or ig_business_profile_all | ig_business_profile_engaged | ig_user_messaged_business | ig_business_profile_visit"
},
"ratio": {
"type": "number",
"description": "lookalike: 0.01–0.20 = the top 1%–20% most similar people (default 0.01)"
},
"pageId": {
"type": "string",
"description": "engagement: which connected Page (required only if the profile has several)"
},
"schema": {
"type": "array",
"items": {
"enum": [
"EMAIL",
"PHONE",
"FN",
"LN",
"FI",
"CT",
"ST",
"ZIP",
"COUNTRY",
"DOB",
"DOBY",
"GEN",
"MADID",
"EXTERN_ID"
],
"type": "string"
},
"description": "customerfile: the column names of rows, in order, e.g. [\"EMAIL\",\"PHONE\"]"
},
"source": {
"enum": [
"page",
"instagram"
],
"type": "string",
"description": "engagement: Facebook Page (default) or the linked Instagram business profile"
},
"country": {
"type": "string",
"description": "lookalike: REQUIRED 2-letter country you will run it in — Meta delivers a lookalike to the locations of the ad set that targets it, so target this country there"
},
"pixelId": {
"type": "string",
"description": "website: the Meta Pixel that sees the traffic"
},
"prefill": {
"type": "boolean",
"description": "website/engagement: seed it with activity from BEFORE the audience existed (default true)"
},
"audienceId": {
"type": "string",
"description": "customerfile: ADD these rows to this existing customer-list audience (from list_meta_audiences) instead of creating a new one"
},
"adAccountId": {
"type": "string",
"description": "ad account id (act_… or digits)"
},
"description": {
"type": "string",
"description": "optional free-text note stored with the audience"
},
"urlContains": {
"type": "string",
"description": "website: the URL fragment that defines the audience (your domain = all visitors)"
},
"lookalikeType": {
"enum": [
"similarity",
"reach"
],
"type": "string",
"description": "lookalike: similarity (tighter) or reach (broader) — default similarity"
},
"retentionDays": {
"type": "number",
"description": "how long someone stays in it — website max 180, engagement max 730 (default 30)"
},
"startingRatio": {
"type": "number",
"description": "lookalike: optional lower bound for a band (e.g. 0.01 with ratio 0.03 = the 1%–3% band) — at least 0.01 and less than ratio; not combinable with lookalikeType"
},
"sourceAudienceId": {
"type": "string",
"description": "lookalike: the existing audience to model (from list_meta_audiences)"
},
"customerFileSource": {
"enum": [
"USER_PROVIDED_ONLY",
"PARTNER_PROVIDED_ONLY",
"BOTH_USER_AND_PARTNER_PROVIDED"
],
"type": "string",
"description": "customerfile: REQUIRED attestation of where the list came from (customers gave it to you = USER_PROVIDED_ONLY)"
}
}
}| productCatalogId | string | CATALOG (dynamic product) ADS. This is the CAMPAIGN-level half of a catalog ad; the AD SET takes a productSetId and the creative takes the same set. Passing a product SET id here is refused by name with the level it belongs on. The binding is READ BACK from Meta before it is reported, so a 200 is never narrated as “bound”. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"adAccountId"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"objective": {
"enum": [
"OUTCOME_TRAFFIC",
"OUTCOME_AWARENESS",
"OUTCOME_ENGAGEMENT",
"OUTCOME_LEADS",
"OUTCOME_SALES",
"OUTCOME_APP_PROMOTION"
],
"type": "string",
"description": "default OUTCOME_TRAFFIC"
},
"adAccountId": {
"type": "string",
"description": "Ad account id"
},
"bidStrategy": {
"enum": [
"LOWEST_COST_WITHOUT_CAP",
"LOWEST_COST_WITH_BID_CAP",
"COST_CAP",
"LOWEST_COST_WITH_MIN_ROAS"
],
"type": "string",
"description": "with dailyBudgetUsd: how the campaign bids across its ad sets — LOWEST_COST_WITHOUT_CAP (default: highest volume), LOWEST_COST_WITH_BID_CAP / COST_CAP (each ad set then needs bidAmountUsd), LOWEST_COST_WITH_MIN_ROAS (each ad set needs minRoas)"
},
"dailyBudgetUsd": {
"type": "number",
"description": "optional campaign daily budget in the AD ACCOUNT's currency (not necessarily USD — the reply names it); real spend once ACTIVE. With it, the campaign holds the budget (Advantage+ campaign budget) and every ad set under it carries NONE — create_meta_adset / create_meta_ad then send no ad-set budget"
},
"productCatalogId": {
"type": "string",
"description": "CATALOG (dynamic product) ADS. This is the CAMPAIGN-level half of a catalog ad; the AD SET takes a productSetId and the creative takes the same set. Passing a product SET id here is refused by name with the level it belongs on. The binding is READ BACK from Meta before it is reported, so a 200 is never narrated as “bound”."
}
}
}| questions* |
| array |
| Meta question types as strings ("FULL_NAME","EMAIL","PHONE","COMPANY_NAME","JOB_TITLE","CITY","ZIP","WORK_EMAIL","DATE_TIME"…) or objects {type,label,key,options[]} for CUSTOM |
| contextCard | object | {title, bullets[], body, buttonText, style} — the screen shown before the questions |
| blockNonTargeted | boolean | true = hide the form from people the ad's targeting does not match |
| privacyPolicyUrl* | string | REQUIRED — the business’s privacy policy URL (https://…) |
| followUpActionUrl | string | https URL the person is sent to after submitting the form |
| trackingParameters | object | extra tracking parameters for the form, as a key/value object |
| isOptimizedForQuality | boolean | adds a review-and-confirm step: fewer leads, better ones |
| privacyPolicyLinkText | string | link text shown for the privacy policy URL on the form |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"questions",
"privacyPolicyUrl"
],
"properties": {
"name": {
"type": "string",
"description": "internal name for the form — not shown to the person filling it in"
},
"locale": {
"type": "string",
"description": "form language, e.g. EN_US, ES_ES, PT_BR, DE_DE"
},
"pageId": {
"type": "string",
"description": "which connected Page (required only if the profile has more than one)"
},
"headline": {
"type": "string",
"description": "headline shown above the questions"
},
"thankYou": {
"type": "object",
"description": "{title, body, buttonType, buttonText, websiteUrl} — shown after submitting",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"questions": {
"type": "array",
"items": {},
"description": "Meta question types as strings (\"FULL_NAME\",\"EMAIL\",\"PHONE\",\"COMPANY_NAME\",\"JOB_TITLE\",\"CITY\",\"ZIP\",\"WORK_EMAIL\",\"DATE_TIME\"…) or objects {type,label,key,options[]} for CUSTOM"
},
"contextCard": {
"type": "object",
"description": "{title, bullets[], body, buttonText, style} — the screen shown before the questions",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"blockNonTargeted": {
"type": "boolean",
"description": "true = hide the form from people the ad's targeting does not match"
},
"privacyPolicyUrl": {
"type": "string",
"description": "REQUIRED — the business’s privacy policy URL (https://…)"
},
"followUpActionUrl": {
"type": "string",
"description": "https URL the person is sent to after submitting the form"
},
"trackingParameters": {
"type": "object",
"description": "extra tracking parameters for the form, as a key/value object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"isOptimizedForQuality": {
"type": "boolean",
"description": "adds a review-and-confirm step: fewer leads, better ones"
},
"privacyPolicyLinkText": {
"type": "string",
"description": "link text shown for the privacy policy URL on the form"
}
}
}| headlines | array | 3–15 headlines, each ≤30 characters |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| descriptions | array | 2–4 descriptions, each ≤90 characters |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId"
],
"properties": {
"path1": {
"type": "string",
"description": "display-URL path segment, ≤15 chars, no \"/\""
},
"path2": {
"type": "string",
"description": "second display-URL path segment (only with path1)"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "default Paused"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create this Active in a LIVE ad group"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "the ad group this ad lives in"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the landing page(s) — at least one is required"
},
"headlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "3–15 headlines, each ≤30 characters"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"descriptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "2–4 descriptions, each ≤90 characters"
}
}
}| string |
| older name for adAccountId (MCP only) |
| campaignId* | string | the campaign this ad group belongs to |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name"
],
"properties": {
"ad": {
"type": "object",
"properties": {
"path1": {
"type": "string",
"description": "display-URL path segment, ≤15 chars, no \"/\""
},
"path2": {
"type": "string",
"description": "second display-URL path segment (only with path1)"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the landing page(s) — at least one is required"
},
"headlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "3–15 headlines, each ≤30 characters"
},
"descriptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "2–4 descriptions, each ≤90 characters"
}
},
"description": "create the responsive search ad in the same call"
},
"name": {
"type": "string",
"description": "ad group name, ≤256 characters"
},
"cpcBid": {
"type": "number",
"description": "default max CPC in the account currency"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "default Paused"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create this Active under a LIVE campaign"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"bid": {
"type": "number",
"description": "per-keyword max CPC in the account currency"
},
"text": {
"type": "string",
"description": "≤100 characters"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "default Paused"
},
"matchType": {
"enum": [
"Exact",
"Phrase",
"Broad"
],
"type": "string",
"description": "default Phrase — Microsoft has no broad-match-modifier"
}
}
},
"description": "keywords to add: [{text, matchType, bid, status}]; status defaults to Paused"
},
"language": {
"type": "string",
"description": "required if the campaign has no language set"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "the campaign this ad group belongs to"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}| number |
| bid cap for MaxClicks / MaxConversions / MaxConversionValue / TargetImpressionShare (not Performance Max) |
| adGroup | object | build the serving tree in the same call — WITHOUT this you get a campaign shell that can never show an ad |
| 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. |
| 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 |
| accountId | string | older name for adAccountId (MCP only) |
| feedLabel | string | Shopping: advertise every product with this feed label (≤20 characters). Not with salesCountryCode. |
| languages | array | campaign languages, e.g. ["English"] |
| locations | array | 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 |
| targetCpa | number | Performance Max with MaxConversions: target cost per acquisition, account currency |
| 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 |
| budgetType | string | default DailyBudgetStandard; Accelerated is Audience-campaign only |
| targetRoas | number | Performance Max with MaxConversionValue: target return on ad spend as a ratio, e.g. 4 = 400% |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| dailyBudget* | number | daily budget in the account’s currency |
| 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. |
| biddingScheme | string | 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 |
| locationIntent | string | default PeopleInOrSearchingForOrViewingPages — someone OUTSIDE the target still sees the ad if they search for the place; PeopleIn restricts to people physically there |
| 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. |
| excludeLocations | array | locations to EXCLUDE from the targeted set |
| salesCountryCode | string | Shopping: two-letter country to filter the store catalog to, e.g. "US". Not with feedLabel. |
| 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. |
| targetAdPosition | string | with TargetImpressionShare; MainLine1 = the very top |
| localInventoryAds | boolean | Shopping: enable local inventory ads (only where Microsoft has enabled the feature for the account) |
| targetImpressionShare | number | 1–100, with biddingScheme TargetImpressionShare |
| finalUrlExpansionOptOut | boolean | Performance Max: true = serve only the final URLs you give; default false lets Microsoft use the whole domain |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"dailyBudget"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name, ≤128 characters"
},
"maxCpc": {
"type": "number",
"description": "bid cap for MaxClicks / MaxConversions / MaxConversionValue / TargetImpressionShare (not Performance Max)"
},
"adGroup": {
"type": "object",
"properties": {
"ad": {
"type": "object",
"properties": {
"path1": {
"type": "string",
"description": "display-URL path segment, ≤15 chars, no \"/\""
},
"path2": {
"type": "string",
"description": "second display-URL path segment (only with path1)"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the landing page(s) — at least one is required"
},
"headlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "3–15 headlines, each ≤30 characters"
},
"descriptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "2–4 descriptions, each ≤90 characters"
}
},
"description": "responsive search ad: headlines, descriptions, finalUrls and optional path1/path2"
},
"name": {
"type": "string",
"description": "ad group name, up to 256 characters"
},
"cpcBid": {
"type": "number",
"description": "default max CPC for the ad group, in account currency"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "Active or Paused for the ad group; the campaign itself is always created Paused"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"bid": {
"type": "number",
"description": "per-keyword max CPC in the account currency"
},
"text": {
"type": "string",
"description": "≤100 characters"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "default Paused"
},
"matchType": {
"enum": [
"Exact",
"Phrase",
"Broad"
],
"type": "string",
"description": "default Phrase — Microsoft has no broad-match-modifier"
}
}
},
"description": "keywords for the ad group, each with text, matchType, bid and status"
},
"language": {
"type": "string",
"description": "ad group language; required if the campaign has no language set"
}
},
"description": "build the serving tree in the same call — WITHOUT this you get a campaign shell that can never show an ad"
},
"storeId": {
"type": "string",
"description": "Shopping (required) or retail Performance Max (optional): the Microsoft Merchant Center store id. Checked against the account’s own stores before anything is created."
},
"timeZone": {
"type": "string",
"description": "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"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"feedLabel": {
"type": "string",
"description": "Shopping: advertise every product with this feed label (≤20 characters). Not with salesCountryCode."
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "campaign languages, e.g. [\"English\"]"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "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"
},
"targetCpa": {
"type": "number",
"description": "Performance Max with MaxConversions: target cost per acquisition, account currency"
},
"worldwide": {
"type": "boolean",
"description": "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"
},
"budgetType": {
"enum": [
"DailyBudgetStandard",
"DailyBudgetAccelerated",
"LifetimeBudgetStandard"
],
"type": "string",
"description": "default DailyBudgetStandard; Accelerated is Audience-campaign only"
},
"targetRoas": {
"type": "number",
"description": "Performance Max with MaxConversionValue: target return on ad spend as a ratio, e.g. 4 = 400%"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"dailyBudget": {
"type": "number",
"description": "daily budget in the account’s currency"
},
"campaignType": {
"type": "string",
"description": "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."
},
"biddingScheme": {
"enum": [
"EnhancedCpc",
"MaxClicks",
"MaxConversions",
"MaxConversionValue",
"TargetImpressionShare",
"TargetCpa",
"TargetRoas"
],
"type": "string",
"description": "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"
},
"locationIntent": {
"enum": [
"PeopleInOrSearchingForOrViewingPages",
"PeopleIn"
],
"type": "string",
"description": "default PeopleInOrSearchingForOrViewingPages — someone OUTSIDE the target still sees the ad if they search for the place; PeopleIn restricts to people physically there"
},
"dynamicSearchAds": {
"type": "object",
"required": [
"domainName",
"language"
],
"properties": {
"source": {
"enum": [
"SystemIndex",
"AdvertiserSuppliedUrls",
"All"
],
"type": "string",
"description": "which pages ads are built from: SystemIndex (Microsoft index), AdvertiserSuppliedUrls or All"
},
"language": {
"type": "string",
"description": "the pages’ language: Dutch, English, French, German, Italian, Spanish or Swedish"
},
"domainName": {
"type": "string",
"description": "the website, e.g. \"example.com\""
},
"dynamicDescriptionEnabled": {
"type": "boolean",
"description": "true = Microsoft generates ad descriptions from page content"
}
},
"description": "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."
},
"excludeLocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "locations to EXCLUDE from the targeted set"
},
"salesCountryCode": {
"type": "string",
"description": "Shopping: two-letter country to filter the store catalog to, e.g. \"US\". Not with feedLabel."
},
"shoppingPriority": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "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."
},
"targetAdPosition": {
"enum": [
"FirstPage",
"MainLine",
"MainLine1"
],
"type": "string",
"description": "with TargetImpressionShare; MainLine1 = the very top"
},
"localInventoryAds": {
"type": "boolean",
"description": "Shopping: enable local inventory ads (only where Microsoft has enabled the feature for the account)"
},
"targetImpressionShare": {
"type": "number",
"description": "1–100, with biddingScheme TargetImpressionShare"
},
"finalUrlExpansionOptOut": {
"type": "boolean",
"description": "Performance Max: true = serve only the final URLs you give; default false lets Microsoft use the whole domain"
}
}
}| older name for adAccountId (MCP only) |
| countType | string | All or Unique: how repeat conversions are counted |
| eventLabel | string | Event goals: UET custom event label |
| eventValue | number | Event goals: numeric value attached to each event |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| eventAction | string | Event goals: UET custom event action |
| revenueType | string | FixedValue (one value per conversion), VariableValue or NoValue |
| urlOperator | string | Url goals: how urlExpression matches the page: Equals, BeginsWith, Contains or RegularExpression |
| currencyCode | string | 3-letter ISO currency code for the revenue value, e.g. USD |
| eventCategory | string | Event goals: UET custom event category |
| urlExpression | string | Url goals: the thank-you page URL or path to match |
| minimumPagesViewed | number | PagesViewedPerVisit goals: page count that counts as a conversion |
| conversionWindowDays | number | 1–90 |
| minimumDurationSeconds | number | Duration goals: visit length in seconds that counts as a conversion |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"type"
],
"properties": {
"name": {
"type": "string",
"description": "≤100 characters, unique per customer"
},
"type": {
"enum": [
"Url",
"Event",
"OfflineConversion",
"Duration",
"PagesViewedPerVisit"
],
"type": "string",
"description": "Url (thank-you page), Event (UET custom event), OfflineConversion, Duration or PagesViewedPerVisit"
},
"scope": {
"enum": [
"Account",
"Customer"
],
"type": "string",
"description": "Account = this ad account only; Customer = shared across the customer"
},
"tagId": {
"type": "string",
"description": "UET tag id from create_microsoft_ads_uet_tag; not needed for OfflineConversion"
},
"revenue": {
"type": "number",
"description": "FixedValue: the value of one conversion; VariableValue: the default"
},
"category": {
"enum": [
"AddToCart",
"BeginCheckout",
"BookAppointment",
"Contact",
"Download",
"GetDirections",
"OutboundClick",
"PageView",
"Purchase",
"RequestQuote",
"Signup",
"SubmitLeadForm",
"Subscribe",
"Other"
],
"type": "string",
"description": "conversion category, e.g. Purchase or Signup; required for Url, Event and OfflineConversion"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"countType": {
"enum": [
"All",
"Unique"
],
"type": "string",
"description": "All or Unique: how repeat conversions are counted"
},
"eventLabel": {
"type": "string",
"description": "Event goals: UET custom event label"
},
"eventValue": {
"type": "number",
"description": "Event goals: numeric value attached to each event"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"eventAction": {
"type": "string",
"description": "Event goals: UET custom event action"
},
"revenueType": {
"enum": [
"FixedValue",
"VariableValue",
"NoValue"
],
"type": "string",
"description": "FixedValue (one value per conversion), VariableValue or NoValue"
},
"urlOperator": {
"enum": [
"Equals",
"BeginsWith",
"Contains",
"RegularExpression"
],
"type": "string",
"description": "Url goals: how urlExpression matches the page: Equals, BeginsWith, Contains or RegularExpression"
},
"currencyCode": {
"type": "string",
"description": "3-letter ISO currency code for the revenue value, e.g. USD"
},
"eventCategory": {
"type": "string",
"description": "Event goals: UET custom event category"
},
"urlExpression": {
"type": "string",
"description": "Url goals: the thank-you page URL or path to match"
},
"minimumPagesViewed": {
"type": "number",
"description": "PagesViewedPerVisit goals: page count that counts as a conversion"
},
"conversionWindowDays": {
"type": "number",
"description": "1–90"
},
"minimumDurationSeconds": {
"type": "number",
"description": "Duration goals: visit length in seconds that counts as a conversion"
}
}
}| older name for adAccountId (MCP only) |
| adGroupId* | string | ad group to place the image ad in |
| finalUrls* | array | landing page URL(s) the ad sends people to |
| headlines* | array | 1-15 headlines, up to 30 characters each |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| businessName* | string | business name shown on the ad, up to 25 characters |
| callToAction | string | Search multimedia ads only, e.g. LearnMore, ShopNow, SignUp |
| descriptions* | array | 1-5 descriptions, up to 90 characters each |
| videoSubType | string | aspect ratio of the attached video: 16x9, 5x4, 1x1, 4x5 or 9x16 |
| longHeadlines* | array | 1-5 long headlines, up to 90 characters each |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"businessName",
"headlines",
"longHeadlines",
"descriptions",
"finalUrls"
],
"properties": {
"images": {
"type": "object",
"properties": {
"logo": {
"type": "string",
"description": "logo picture URL"
},
"square": {
"type": "string",
"description": "1:1 picture URL"
},
"landscape": {
"type": "string",
"description": "1.91:1 picture URL"
}
},
"description": "pictures by shape: {landscape, square, logo} URLs, instead of imageUrl"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "default Paused"
},
"confirm": {
"type": "boolean",
"description": "true required to create the ad Active in a live ad group"
},
"logoUrl": {
"type": "string",
"description": "logo picture URL"
},
"imageUrl": {
"type": "string",
"description": "one picture, cut to both shapes for you"
},
"videoIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "video ids from upload_microsoft_ads_video to attach"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "ad group to place the image ad in"
},
"finalUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "landing page URL(s) the ad sends people to"
},
"headlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "1-15 headlines, up to 30 characters each"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"businessName": {
"type": "string",
"description": "business name shown on the ad, up to 25 characters"
},
"callToAction": {
"type": "string",
"description": "Search multimedia ads only, e.g. LearnMore, ShopNow, SignUp"
},
"descriptions": {
"type": "array",
"items": {
"type": "string"
},
"description": "1-5 descriptions, up to 90 characters each"
},
"videoSubType": {
"enum": [
"VideoMedia16X9",
"VideoMedia5X4",
"VideoMedia1X1",
"VideoMedia4X5",
"VideoMedia9X16"
],
"type": "string",
"description": "aspect ratio of the attached video: 16x9, 5x4, 1x1, 4x5 or 9x16"
},
"longHeadlines": {
"type": "array",
"items": {
"type": "string"
},
"description": "1-5 long headlines, up to 90 characters each"
}
}
}| string |
| ad group id to create the ad in |
| 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"creative"
],
"properties": {
"name": {
"type": "string",
"description": "internal name — defaults to the title"
},
"status": {
"enum": [
"active",
"paused"
],
"type": "string",
"description": "default paused"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create this ACTIVE in a live ad group (real spend)"
},
"creative": {
"type": "object",
"required": [
"title",
"body",
"targetUrl"
],
"properties": {
"body": {
"type": "string",
"description": "the description under the headline — 100 characters maximum, enforced"
},
"price": {
"type": "string",
"description": "optional price string shown on the card"
},
"title": {
"type": "string",
"description": "the headline — 3 to 50 characters, enforced"
},
"fileId": {
"type": "string",
"description": "reuse an image ChatGPT Ads already holds: the fileId list_openai_ads_campaigns returns for an existing ad (list with adGroupId). Wins over imageUrl; use it to copy an ad exactly"
},
"fileIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the same as imageUrls with images already on the account (4–6 file ids); may be mixed with imageUrls, ids first. On an edit, omit both to keep the ad’s current images; a new list replaces them all"
},
"imageUrl": {
"type": "string",
"description": "public https URL of a STILL image — every text ad needs imageUrl OR fileId (ChatGPT Ads refuses an ad with no creative.file_id unless the campaign is a product feed; measured 2026-09-06); a video URL is refused, this channel has no video format"
},
"imageUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "4–6 public https URLs of EXACTLY square (1:1) stills, in display order, for an ad with MULTIPLE images (one title, body and landing page shared by all). Each URL is measured before anything is uploaded: OpenAI refuses an image even 1 pixel off square, and a set of 16:9 images. OpenAI enables this per ad account, and with automatic bidding only if the account is also enabled for that — otherwise use a fixed bid. Not with imageUrl/fileId or price"
},
"targetUrl": {
"type": "string",
"description": "the landing page (must not block OAI-AdsBot / OAI-SearchBot in robots.txt)"
}
},
"description": "title 3-50 characters, body up to 100, landing page URL and one still image; no video"
},
"adGroupId": {
"type": "string",
"description": "ad group id to create the ad in"
},
"landingPageQueryTemplate": {
"type": "string",
"description": "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."
}
}
}| REQUIRED true to create this ACTIVE under a live campaign (real spend) |
| targetCpa | number | Target CPA in the account currency: the average cost per conversion OpenAI aims for (bidStrategy target_cpa, conversions campaign, from 2026-11-09). Billing stays per click. Not with maxBid. |
| targetCpc | number | Target CPC in the account currency: the average cost per click OpenAI aims for (bidStrategy target_cpc, clicks campaign, from 2026-11-09). Not with maxBid. |
| campaignId* | string | campaign id to add the ad group to |
| bidStrategy | string | HOW THIS AD GROUP BIDS. OMIT IT (and maxBid) and a clicks or conversions campaign with a DAILY budget gets INTELLIGENT BIDDING — OpenAI’s new name for “Maximize results”: maximize_clicks / maximize_conversions, no amount. From 2026-11-09 OpenAI adds COST TARGETS: target_cpa with targetCpa (conversions campaigns) and target_cpc with targetCpc (clicks campaigns); OpenAI may bid above or below the target in any one auction, so set it from the recent actual CPA/CPC in openai_ads_report. An account OpenAI has not switched on yet is refused with that date. fixed_bid (with maxBid) is Max Bid / Max CPC: DEPRECATED, retired by ChatGPT Ads on 2027-01-01 and refused from then — do not choose it for a new ad group except on an impressions campaign. The strategy must match the campaign: maximize_clicks / target_cpc need biddingType clicks, maximize_conversions / target_cpa need conversions. |
| description | string | optional ad group description |
| billingEvent | string | default click |
| contextHints | array | up to 2,000, deduplicated server-side |
| audienceBidMultipliers | array | 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. |
| 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "ad group name"
},
"maxBid": {
"type": "number",
"description": "DEPRECATED — Max Bid / Max CPC, retired by ChatGPT Ads on 2027-01-01: a fixed bid in the account currency (bidStrategy fixed_bid; on a conversions campaign it is the CPA bid). Needed only on an impressions campaign (per impression); otherwise use intelligent bidding or a target."
},
"status": {
"enum": [
"active",
"paused"
],
"type": "string",
"description": "default paused"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create this ACTIVE under a live campaign (real spend)"
},
"targetCpa": {
"type": "number",
"description": "Target CPA in the account currency: the average cost per conversion OpenAI aims for (bidStrategy target_cpa, conversions campaign, from 2026-11-09). Billing stays per click. Not with maxBid."
},
"targetCpc": {
"type": "number",
"description": "Target CPC in the account currency: the average cost per click OpenAI aims for (bidStrategy target_cpc, clicks campaign, from 2026-11-09). Not with maxBid."
},
"campaignId": {
"type": "string",
"description": "campaign id to add the ad group to"
},
"bidStrategy": {
"enum": [
"fixed_bid",
"maximize_clicks",
"maximize_conversions",
"target_cpc",
"target_cpa"
],
"type": "string",
"description": "HOW THIS AD GROUP BIDS. OMIT IT (and maxBid) and a clicks or conversions campaign with a DAILY budget gets INTELLIGENT BIDDING — OpenAI’s new name for “Maximize results”: maximize_clicks / maximize_conversions, no amount. From 2026-11-09 OpenAI adds COST TARGETS: target_cpa with targetCpa (conversions campaigns) and target_cpc with targetCpc (clicks campaigns); OpenAI may bid above or below the target in any one auction, so set it from the recent actual CPA/CPC in openai_ads_report. An account OpenAI has not switched on yet is refused with that date. fixed_bid (with maxBid) is Max Bid / Max CPC: DEPRECATED, retired by ChatGPT Ads on 2027-01-01 and refused from then — do not choose it for a new ad group except on an impressions campaign. The strategy must match the campaign: maximize_clicks / target_cpc need biddingType clicks, maximize_conversions / target_cpa need conversions."
},
"description": {
"type": "string",
"description": "optional ad group description"
},
"billingEvent": {
"enum": [
"click",
"impression"
],
"type": "string",
"description": "default click"
},
"contextHints": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 2,000, deduplicated server-side"
},
"audienceBidMultipliers": {
"type": "array",
"items": {
"type": "object",
"required": [
"customAudienceId",
"multiplier"
],
"properties": {
"multiplier": {
"type": "number",
"description": "0.1 to 10; 2 bids twice as much for this audience"
},
"customAudienceId": {
"type": "string",
"description": "custom audience id to bid more or less for, from list_openai_ads_audiences"
}
}
},
"description": "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."
},
"landingPageQueryTemplate": {
"type": "string",
"description": "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."
}
}
}| identifierResolution |
| string |
| auto = read every identifier column of a mixed CSV |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "audience name"
},
"fileUrl": {
"type": "string",
"description": "public link to a UTF-8 .csv or .txt customer list; upload_file turns a local file into one"
},
"members": {
"type": "array",
"items": {
"type": "string"
},
"description": "emails and/or phone numbers (already-SHA256-hashed emails are passed through as-is); or use fileUrl"
},
"fileName": {
"type": "string",
"description": "the file name with .csv or .txt, when the link hides it"
},
"description": {
"type": "string",
"description": "optional audience description"
},
"identifierType": {
"enum": [
"email",
"phone",
"email_sha256",
"phone_number_sha256",
"gaid"
],
"type": "string",
"description": "the one identifier the file holds (inferred from a CSV header when omitted)"
},
"identifierResolution": {
"enum": [
"auto"
],
"type": "string",
"description": "auto = read every identifier column of a mixed CSV"
}
}
}| businessAgentId | string | replace THIS agent’s configuration instead of creating a new one |
| privacyPolicyUrl | string | privacy policy URL shown with the agent |
| conversationStarters | array | suggested opening questions shown to users |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"instructions"
],
"properties": {
"name": {
"type": "string",
"description": "business agent name"
},
"toolIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "tool ids the agent may use, from list_openai_ads_business_agent_tools"
},
"leadFormId": {
"type": "string",
"description": "published lead form id the agent collects leads with"
},
"description": {
"type": "string",
"description": "optional description of the agent"
},
"connectorIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "connector ids the agent may use"
},
"instructions": {
"type": "string",
"description": "system prompt that users experience, written in the brand's voice"
},
"productFeedIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "product feed ids the agent may draw on, from list_openai_ads_feeds"
},
"businessAgentId": {
"type": "string",
"description": "replace THIS agent’s configuration instead of creating a new one"
},
"privacyPolicyUrl": {
"type": "string",
"description": "privacy policy URL shown with the agent"
},
"conversationStarters": {
"type": "array",
"items": {
"type": "string"
},
"description": "suggested opening questions shown to users"
}
}
}| string |
| campaign name, at least 3 characters |
| adGroup | object | build the ad group (and its ad) in the same call — a campaign alone cannot serve |
| endTime | number | unix seconds |
| countries | array | 2-letter country codes |
| objective | string | the campaign objective. objective "conversions" with billingEventType "impression" is the impression-billed, conversion-optimised shape |
| platforms | array | 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. |
| startTime | number | unix seconds |
| biddingType | string | default clicks (CPC). "conversions" is oCPC — you still pay per click, but ChatGPT Ads optimises toward a conversion event, and it REQUIRES conversionEventSettingIds naming exactly one active event setting. |
| dailyBudget | number | daily cap in the AD ACCOUNT’S currency — ChatGPT Ads’ own minimum for a DAILY budget is 25.00 |
| description | string | optional campaign description |
| locationIds | array | ids from openai_ads_geo_search — up to 2,500 |
| 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 |
| billingEventType | string | what the campaign is billed on |
| customAudienceIds | array | 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. |
| 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 |
| conversionEventSettingIds | array | 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. |
| excludedCustomAudienceIds | array | 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name, at least 3 characters"
},
"adGroup": {
"type": "object",
"required": [
"name"
],
"properties": {
"ad": {
"type": "object",
"required": [
"creative"
],
"properties": {
"name": {
"type": "string",
"description": "internal ad name; defaults to the title"
},
"creative": {
"type": "object",
"required": [
"title",
"body",
"targetUrl"
],
"properties": {
"body": {
"type": "string",
"description": "the description under the headline — 100 characters maximum, enforced"
},
"price": {
"type": "string",
"description": "optional price string shown on the card"
},
"title": {
"type": "string",
"description": "the headline — 3 to 50 characters, enforced"
},
"fileId": {
"type": "string",
"description": "reuse an image ChatGPT Ads already holds: the fileId list_openai_ads_campaigns returns for an existing ad (list with adGroupId). Wins over imageUrl; use it to copy an ad exactly"
},
"fileIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the same as imageUrls with images already on the account (4–6 file ids); may be mixed with imageUrls, ids first. On an edit, omit both to keep the ad’s current images; a new list replaces them all"
},
"imageUrl": {
"type": "string",
"description": "public https URL of a STILL image — every text ad needs imageUrl OR fileId (ChatGPT Ads refuses an ad with no creative.file_id unless the campaign is a product feed; measured 2026-09-06); a video URL is refused, this channel has no video format"
},
"imageUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "4–6 public https URLs of EXACTLY square (1:1) stills, in display order, for an ad with MULTIPLE images (one title, body and landing page shared by all). Each URL is measured before anything is uploaded: OpenAI refuses an image even 1 pixel off square, and a set of 16:9 images. OpenAI enables this per ad account, and with automatic bidding only if the account is also enabled for that — otherwise use a fixed bid. Not with imageUrl/fileId or price"
},
"targetUrl": {
"type": "string",
"description": "the landing page (must not block OAI-AdsBot / OAI-SearchBot in robots.txt)"
}
},
"description": "title 3-50 characters, body up to 100, landing page URL and one still image; no video"
}
},
"description": "the ad created inside the ad group; carries the creative"
},
"name": {
"type": "string",
"description": "ad group name"
},
"maxBid": {
"type": "number",
"description": "DEPRECATED — Max Bid / Max CPC, retired by ChatGPT Ads on 2027-01-01: a fixed bid in the account currency (bidStrategy fixed_bid; on a conversions campaign it is the CPA bid). Needed only on an impressions campaign (per impression); otherwise use intelligent bidding or a target."
},
"targetCpa": {
"type": "number",
"description": "Target CPA in the account currency: the average cost per conversion OpenAI aims for (bidStrategy target_cpa, conversions campaign, from 2026-11-09). Billing stays per click. Not with maxBid."
},
"targetCpc": {
"type": "number",
"description": "Target CPC in the account currency: the average cost per click OpenAI aims for (bidStrategy target_cpc, clicks campaign, from 2026-11-09). Not with maxBid."
},
"bidStrategy": {
"enum": [
"fixed_bid",
"maximize_clicks",
"maximize_conversions",
"target_cpc",
"target_cpa"
],
"type": "string",
"description": "HOW THIS AD GROUP BIDS. OMIT IT (and maxBid) and a clicks or conversions campaign with a DAILY budget gets INTELLIGENT BIDDING — OpenAI’s new name for “Maximize results”: maximize_clicks / maximize_conversions, no amount. From 2026-11-09 OpenAI adds COST TARGETS: target_cpa with targetCpa (conversions campaigns) and target_cpc with targetCpc (clicks campaigns); OpenAI may bid above or below the target in any one auction, so set it from the recent actual CPA/CPC in openai_ads_report. An account OpenAI has not switched on yet is refused with that date. fixed_bid (with maxBid) is Max Bid / Max CPC: DEPRECATED, retired by ChatGPT Ads on 2027-01-01 and refused from then — do not choose it for a new ad group except on an impressions campaign. The strategy must match the campaign: maximize_clicks / target_cpc need biddingType clicks, maximize_conversions / target_cpa need conversions."
},
"description": {
"type": "string",
"description": "optional ad group description"
},
"billingEvent": {
"enum": [
"click",
"impression"
],
"type": "string",
"description": "what the ad group is billed on: click (default) or impression"
},
"contextHints": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 2,000 natural-language conversation/topic descriptions — make them genuinely distinct from each other"
}
},
"description": "build the ad group (and its ad) in the same call — a campaign alone cannot serve"
},
"endTime": {
"type": "number",
"description": "unix seconds"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-letter country codes"
},
"objective": {
"enum": [
"reach",
"clicks",
"conversions"
],
"type": "string",
"description": "the campaign objective. objective \"conversions\" with billingEventType \"impression\" is the impression-billed, conversion-optimised shape"
},
"platforms": {
"type": "array",
"items": {
"enum": [
"ios_app",
"android_app",
"web",
"desktop_web",
"ios_web",
"android_web"
],
"type": "string"
},
"description": "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."
},
"startTime": {
"type": "number",
"description": "unix seconds"
},
"biddingType": {
"enum": [
"impressions",
"clicks",
"conversions"
],
"type": "string",
"description": "default clicks (CPC). \"conversions\" is oCPC — you still pay per click, but ChatGPT Ads optimises toward a conversion event, and it REQUIRES conversionEventSettingIds naming exactly one active event setting."
},
"dailyBudget": {
"type": "number",
"description": "daily cap in the AD ACCOUNT’S currency — ChatGPT Ads’ own minimum for a DAILY budget is 25.00"
},
"description": {
"type": "string",
"description": "optional campaign description"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ids from openai_ads_geo_search — up to 2,500"
},
"lifetimeBudget": {
"type": "number",
"description": "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": {
"type": "string",
"description": "run a BUSINESS AGENT campaign (mode business_agent): the id from create_openai_ads_business_agent, which must be PUBLISHED"
},
"billingEventType": {
"enum": [
"impression",
"click"
],
"type": "string",
"description": "what the campaign is billed on"
},
"customAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"landingPageQueryTemplate": {
"type": "string",
"description": "query-string template appended to landing URLs (e.g. utm_source=chatgpt&utm_campaign={campaign_id}) — ChatGPT Ads’ landing_page_configuration.query_string_template"
},
"conversionEventSettingIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"excludedCustomAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
}
}
}| creativeType | string | default REGULAR |
| destinationUrl | string | where the click goes |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"pinId"
],
"properties": {
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term tag merged into destinationUrl; Pinterest has no UTM field"
},
"medium": {
"type": "string",
"description": "utm_medium tag merged into destinationUrl; Pinterest has no UTM field"
},
"source": {
"type": "string",
"description": "utm_source tag merged into destinationUrl; Pinterest has no UTM field"
},
"content": {
"type": "string",
"description": "utm_content tag merged into destinationUrl; Pinterest has no UTM field"
},
"campaign": {
"type": "string",
"description": "utm_campaign tag merged into destinationUrl; Pinterest has no UTM field"
}
},
"description": "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"
},
"name": {
"type": "string",
"description": "internal ad name"
},
"pinId": {
"type": "string",
"description": "The numeric id of an existing Pin"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"DRAFT"
],
"type": "string",
"description": "default PAUSED"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create this ACTIVE in a live ad group"
},
"adGroupId": {
"type": "string",
"description": "the ad group this ad lives in"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id, from list_pinterest_ads_campaigns"
},
"creativeType": {
"enum": [
"REGULAR",
"VIDEO",
"SHOPPING",
"CAROUSEL",
"MAX_VIDEO",
"COLLECTION",
"IDEA",
"SHOWCASE",
"QUIZ",
"COLLAGE",
"APP"
],
"type": "string",
"description": "default REGULAR"
},
"destinationUrl": {
"type": "string",
"description": "where the click goes"
}
}
}| confirm | boolean | REQUIRED true to create this ACTIVE under a LIVE campaign |
| campaignId* | string | the campaign this ad group belongs to |
| adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
| billableEvent | string | 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. |
| 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. |
| placementGroup | string | where the ads run: ALL, SEARCH, BROWSE or OTHER |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name"
],
"properties": {
"ad": {
"type": "object",
"required": [
"pinId"
],
"properties": {
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term tag merged into destinationUrl; Pinterest has no UTM field"
},
"medium": {
"type": "string",
"description": "utm_medium tag merged into destinationUrl; Pinterest has no UTM field"
},
"source": {
"type": "string",
"description": "utm_source tag merged into destinationUrl; Pinterest has no UTM field"
},
"content": {
"type": "string",
"description": "utm_content tag merged into destinationUrl; Pinterest has no UTM field"
},
"campaign": {
"type": "string",
"description": "utm_campaign tag merged into destinationUrl; Pinterest has no UTM field"
}
},
"description": "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"
},
"name": {
"type": "string",
"description": "internal ad name"
},
"pinId": {
"type": "string",
"description": "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": {
"enum": [
"REGULAR",
"VIDEO",
"SHOPPING",
"CAROUSEL",
"MAX_VIDEO",
"COLLECTION",
"IDEA",
"SHOWCASE",
"QUIZ",
"COLLAGE",
"APP"
],
"type": "string",
"description": "default REGULAR"
},
"destinationUrl": {
"type": "string",
"description": "where the click goes"
}
},
"description": "create the ad in the same call"
},
"bid": {
"type": "number",
"description": "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."
},
"name": {
"type": "string",
"description": "ad group name"
},
"budget": {
"type": "number",
"description": "ad-group budget — only valid when the campaign is NOT budget-optimized (Pinterest optimizes at campaign level by default)"
},
"pacing": {
"enum": [
"STANDARD",
"ACCELERATED"
],
"type": "string",
"description": "delivery speed: STANDARD or ACCELERATED"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"DRAFT"
],
"type": "string",
"description": "default PAUSED"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create this ACTIVE under a LIVE campaign"
},
"campaignId": {
"type": "string",
"description": "the campaign this ad group belongs to"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id, from list_pinterest_ads_campaigns"
},
"billableEvent": {
"enum": [
"CLICKTHROUGH",
"IMPRESSION",
"VIDEO_V_50_MRC"
],
"type": "string",
"description": "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."
},
"targetingSpec": {
"type": "object",
"description": "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.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"placementGroup": {
"enum": [
"ALL",
"SEARCH",
"BROWSE",
"OTHER"
],
"type": "string",
"description": "where the ads run: ALL, SEARCH, BROWSE or OTHER"
}
}
}| string |
| ACTALIKE |
| percentage | number | ACTALIKE: 1 (most similar) to 10 (widest) |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| description | string | optional note describing the audience |
| engagerType | number | ENGAGEMENT: 1 or 2 |
| audienceType* | string | audience kind; decides which other fields are required (see tool description) |
| eventSources | array | web, mobile and/or offline: where the audience's events come from |
| retentionDays | number | VISITOR / ENGAGEMENT: 1–540, Pinterest default 180 |
| customerListId | string | CUSTOMER_LIST: id of an existing customer list to target |
| engagementType | string | click, save, closeup, comment or like; omit to include all engagements |
| seedAudienceIds | array | ACTALIKE: the audience(s) to find people similar to |
| conversionTagIds | array | VISITOR: the tag(s) whose visitors to collect |
| ingestionSources | array | tag, mmp, file_upload and/or conversions_api: how events reach Pinterest |
| engagementDomains | array | ENGAGEMENT: REQUIRED — your claimed website domain |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"audienceType"
],
"properties": {
"name": {
"type": "string",
"description": "audience name"
},
"rule": {
"type": "object",
"description": "raw Pinterest rule fields, merged last — expert use",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"event": {
"type": "string",
"description": "VISITOR: pagevisit, signup, checkout, viewcategory, search, addtocart, watchvideo, lead, custom (or a partner-defined tag event)"
},
"maids": {
"type": "array",
"items": {
"type": "string"
},
"description": "CUSTOMER_LIST: mobile ad ids to upload as a new list, hashed on our server"
},
"emails": {
"type": "array",
"items": {
"type": "string"
},
"description": "CUSTOMER_LIST: upload these as a new list (hashed here) and make the audience in one call"
},
"pinIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ENGAGEMENT: only people who engaged with these Pins"
},
"country": {
"enum": [
"US",
"CA",
"GB"
],
"type": "string",
"description": "ACTALIKE"
},
"percentage": {
"type": "number",
"description": "ACTALIKE: 1 (most similar) to 10 (widest)"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"description": {
"type": "string",
"description": "optional note describing the audience"
},
"engagerType": {
"type": "number",
"description": "ENGAGEMENT: 1 or 2"
},
"audienceType": {
"enum": [
"VISITOR",
"ENGAGEMENT",
"ACTALIKE",
"CUSTOMER_LIST",
"PERSONA"
],
"type": "string",
"description": "audience kind; decides which other fields are required (see tool description)"
},
"eventSources": {
"type": "array",
"items": {
"enum": [
"web",
"mobile",
"offline"
],
"type": "string"
},
"description": "web, mobile and/or offline: where the audience's events come from"
},
"retentionDays": {
"type": "number",
"description": "VISITOR / ENGAGEMENT: 1–540, Pinterest default 180"
},
"customerListId": {
"type": "string",
"description": "CUSTOMER_LIST: id of an existing customer list to target"
},
"engagementType": {
"enum": [
"click",
"save",
"closeup",
"comment",
"like"
],
"type": "string",
"description": "click, save, closeup, comment or like; omit to include all engagements"
},
"seedAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ACTALIKE: the audience(s) to find people similar to"
},
"conversionTagIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "VISITOR: the tag(s) whose visitors to collect"
},
"ingestionSources": {
"type": "array",
"items": {
"enum": [
"tag",
"mmp",
"file_upload",
"conversions_api"
],
"type": "string"
},
"description": "tag, mmp, file_upload and/or conversions_api: how events reach Pinterest"
},
"engagementDomains": {
"type": "array",
"items": {
"type": "string"
},
"description": "ENGAGEMENT: REQUIRED — your claimed website domain"
}
}
}| number |
| Unix timestamp in SECONDS |
| bidOptions | object | Pinterest bid_options object: multipliers by age, gender, placement, audience or frequency |
| adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
| dailyBudget | number | daily cap in the ad account’s currency |
| isTopOfSearch | boolean | top-of-search campaign |
| isLtvOptimized | boolean | optimize for long-term value |
| 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 |
| intendedPromotionType | string | the kind of ads it will carry |
| isFlexibleDailyBudgets | boolean | flexible daily budgets |
| isCampaignBudgetOptimization | boolean | default true at Pinterest: the budget sits on the campaign |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"objective"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name, ≤255 characters"
},
"adGroup": {
"type": "object",
"properties": {
"ad": {
"type": "object",
"required": [
"pinId"
],
"properties": {
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term tag merged into destinationUrl; Pinterest has no UTM field"
},
"medium": {
"type": "string",
"description": "utm_medium tag merged into destinationUrl; Pinterest has no UTM field"
},
"source": {
"type": "string",
"description": "utm_source tag merged into destinationUrl; Pinterest has no UTM field"
},
"content": {
"type": "string",
"description": "utm_content tag merged into destinationUrl; Pinterest has no UTM field"
},
"campaign": {
"type": "string",
"description": "utm_campaign tag merged into destinationUrl; Pinterest has no UTM field"
}
},
"description": "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"
},
"name": {
"type": "string",
"description": "internal ad name"
},
"pinId": {
"type": "string",
"description": "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": {
"enum": [
"REGULAR",
"VIDEO",
"SHOPPING",
"CAROUSEL",
"MAX_VIDEO",
"COLLECTION",
"IDEA",
"SHOWCASE",
"QUIZ",
"COLLAGE",
"APP"
],
"type": "string",
"description": "default REGULAR"
},
"destinationUrl": {
"type": "string",
"description": "where the click goes"
}
},
"description": "the ad created in this ad group; promotes an existing Pin"
},
"bid": {
"type": "number",
"description": "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."
},
"name": {
"type": "string",
"description": "ad group name"
},
"budget": {
"type": "number",
"description": "ad-group budget — only valid when the campaign is NOT budget-optimized (Pinterest optimizes at campaign level by default)"
},
"pacing": {
"enum": [
"STANDARD",
"ACCELERATED"
],
"type": "string",
"description": "delivery speed: STANDARD or ACCELERATED"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"DRAFT"
],
"type": "string",
"description": "default PAUSED"
},
"billableEvent": {
"enum": [
"CLICKTHROUGH",
"IMPRESSION",
"VIDEO_V_50_MRC"
],
"type": "string",
"description": "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."
},
"targetingSpec": {
"type": "object",
"description": "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.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"placementGroup": {
"enum": [
"ALL",
"SEARCH",
"BROWSE",
"OTHER"
],
"type": "string",
"description": "where the ads run: ALL, SEARCH, BROWSE or OTHER"
}
},
"description": "build the serving tree in the same call — WITHOUT this you get a campaign shell that can never show an ad"
},
"endTime": {
"type": "number",
"description": "Unix timestamp in SECONDS"
},
"objective": {
"enum": [
"AWARENESS",
"CONSIDERATION",
"WEB_CONVERSION",
"CATALOG_SALES",
"VIDEO_COMPLETION",
"APP_INSTALL",
"SALES",
"LEADS",
"CTV_CONSIDERATION"
],
"type": "string",
"description": "Pinterest requires an objective and will not guess one"
},
"startTime": {
"type": "number",
"description": "Unix timestamp in SECONDS"
},
"bidOptions": {
"type": "object",
"description": "Pinterest bid_options object: multipliers by age, gender, placement, audience or frequency",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id, from list_pinterest_ads_campaigns"
},
"dailyBudget": {
"type": "number",
"description": "daily cap in the ad account’s currency"
},
"isTopOfSearch": {
"type": "boolean",
"description": "top-of-search campaign"
},
"isLtvOptimized": {
"type": "boolean",
"description": "optimize for long-term value"
},
"lifetimeBudget": {
"type": "number",
"description": "lifetime cap instead of a daily one — Pinterest then requires endTime"
},
"isPerformancePlus": {
"type": "boolean",
"description": "Pinterest Performance+ on this campaign; with no lifetime budget Pinterest also requires isFlexibleDailyBudgets:true"
},
"isAutomatedCampaign": {
"type": "boolean",
"description": "Pinterest automated campaign"
},
"intendedPromotionType": {
"enum": [
"LEGACY",
"STANDARD_AD",
"CATALOG"
],
"type": "string",
"description": "the kind of ads it will carry"
},
"isFlexibleDailyBudgets": {
"type": "boolean",
"description": "flexible daily budgets"
},
"isCampaignBudgetOptimization": {
"type": "boolean",
"description": "default true at Pinterest: the budget sits on the campaign"
}
}
}| trackingUrl |
| string |
| tracking URL appended to clicks on these promotions |
| creativeType | string | SHOPPING, COLLECTION, CAROUSEL or MAX_VIDEO |
| productGroupIds* | array | catalog product group ids to promote |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"productGroupIds"
],
"properties": {
"bid": {
"type": "number",
"description": "bid in the ad account currency"
},
"cta": {
"type": "string",
"description": "catalog sales CTAs: SHOP_NOW, BOOK_NOW, ON_SALE, GET_DEAL"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED"
],
"type": "string",
"description": "default PAUSED"
},
"confirm": {
"type": "boolean",
"description": "true required to create ACTIVE promotions under a live campaign and ad group"
},
"included": {
"type": "boolean",
"description": "false excludes these product groups instead of including them"
},
"adGroupId": {
"type": "string",
"description": "CATALOG_SALES ad group to attach the product groups to"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"trackingUrl": {
"type": "string",
"description": "tracking URL appended to clicks on these promotions"
},
"creativeType": {
"enum": [
"SHOPPING",
"COLLECTION",
"CAROUSEL",
"MAX_VIDEO"
],
"type": "string",
"description": "SHOPPING, COLLECTION, CAROUSEL or MAX_VIDEO"
},
"productGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "catalog product group ids to promote"
}
}
}| dashboardId |
| number |
| REQUIRED when scope is "dashboard" |
| creationType | string | USR = a person wrote it, GIT = a bot or deployment did |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"content"
],
"properties": {
"scope": {
"enum": [
"project",
"organization",
"dashboard",
"dashboard_item"
],
"type": "string",
"description": "default project. PostHog's `recording` scope is deprecated and rejected by PostHog"
},
"content": {
"type": "string",
"description": "the text shown on the chart, ≤8192 characters"
},
"insightId": {
"type": "number",
"description": "REQUIRED when scope is \"dashboard_item\" — the numeric id of the insight"
},
"dateMarker": {
"type": "string",
"description": "ISO 8601 — defaults to now, which is right for \"this just went live\""
},
"dashboardId": {
"type": "number",
"description": "REQUIRED when scope is \"dashboard\""
},
"creationType": {
"enum": [
"USR",
"GIT"
],
"type": "string",
"description": "USR = a person wrote it, GIT = a bot or deployment did"
}
}
}| adGroupId* | string | the ad group whose targeting and budget this ad runs under |
| productIds | array | catalog ads: up to 6 product ids to feature |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| previewDays | number | 1-30: switch on a preview link that stays valid this many days |
| eventTrackers | array | third-party measurement URLs; only Reddit-approved providers are accepted |
| redditProfileId | string | the post author profile — required for catalog sales campaigns |
| shoppingCreative | object | catalog (shopping) ads only |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "ad name"
},
"postId": {
"type": "string",
"description": "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": {
"type": "string",
"description": "leave unset for TEXT-post ads — Reddit rejects \"Free form ads cannot have a click url\""
},
"adGroupId": {
"type": "string",
"description": "the ad group whose targeting and budget this ad runs under"
},
"productIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "catalog ads: up to 6 product ids to feature"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"previewDays": {
"type": "number",
"description": "1-30: switch on a preview link that stays valid this many days"
},
"eventTrackers": {
"type": "array",
"items": {
"type": "object",
"required": [
"type",
"url"
],
"properties": {
"url": {
"type": "string",
"description": "third-party measurement URL from a Reddit-approved provider"
},
"type": {
"enum": [
"CLICK",
"VIEW"
],
"type": "string",
"description": "CLICK or VIEW: when the third-party tracker fires"
}
}
},
"description": "third-party measurement URLs; only Reddit-approved providers are accepted"
},
"redditProfileId": {
"type": "string",
"description": "the post author profile — required for catalog sales campaigns"
},
"shoppingCreative": {
"type": "object",
"properties": {
"headline": {
"type": "string",
"description": "catalog ad headline"
},
"heroCard": {
"type": "object",
"required": [
"destinationUrl"
],
"properties": {
"imageUrl": {
"type": "string",
"description": "public image URL for the collection intro card"
},
"destinationUrl": {
"type": "string",
"description": "where a click on the collection intro card goes"
}
},
"description": "makes it a collection ad with an intro card"
},
"callToAction": {
"type": "string",
"description": "Reddit’s exact CTA text, e.g. \"Shop Now\""
},
"allowComments": {
"type": "boolean",
"description": "true allows comments on the catalog ad"
},
"secondLineCta": {
"enum": [
"DESCRIPTION",
"PRICE"
],
"type": "string",
"description": "PRICE or DESCRIPTION: text on the second line of the ad"
},
"destinationUrl": {
"type": "string",
"description": "empty = each product’s own page"
},
"dpaCarouselMode": {
"enum": [
"AUTO",
"CAROUSEL_ONLY",
"CAROUSEL_DISABLED"
],
"type": "string",
"description": "AUTO, CAROUSEL_ONLY or CAROUSEL_DISABLED for the product carousel"
}
},
"description": "catalog (shopping) ads only"
},
"clickUrlQueryParameters": {
"type": "object",
"description": "UTM / tracking parameters Reddit appends to the click URL, e.g. {\"utm_source\":\"reddit\",\"utm_medium\":\"paid\",\"utm_content\":\"{{AD_ID}}\"} — up to 14",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
}
}
}| string |
| default DAILY_SPEND; under a CBO campaign it is taken from the campaign |
| schedule | array | weekly dayparting windows — omit to run all week |
| 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 |
| targeting | object | targeting block; every key optional (communities, geolocations, interests, keywords, audiences, devices) |
| campaignId* | string | the campaign this ad group belongs to |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| bidStrategy | string | required — MANUAL_BIDDING needs bidAmount; under a CBO campaign it is taken from the campaign |
| 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 | string | 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 |
| savedAudienceId | string | reuse a saved audience instead of spelling targeting out — from list_reddit_ads_saved_audiences |
| 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 |
| conversionPixelId | string | only needed when the ad account has more than one pixel |
| viewThroughConversionType | string | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name"
],
"properties": {
"name": {
"type": "string",
"description": "ad group name"
},
"budget": {
"type": "number",
"description": "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"
},
"bidType": {
"enum": [
"CPC",
"CPM",
"CPV",
"CPV6",
"CPV15"
],
"type": "string",
"description": "required — must fit the campaign objective, CLICKS campaigns take CPC; under a CBO campaign it is taken from the campaign"
},
"endTime": {
"type": "string",
"description": "ISO 8601 end time; omit for no end"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "default DAILY_SPEND; under a CBO campaign it is taken from the campaign"
},
"schedule": {
"type": "array",
"items": {
"type": "object",
"required": [
"startDay",
"endDay",
"startHour",
"endHour"
],
"properties": {
"endDay": {
"type": "number",
"description": "last weekday of the window, 0 = Sunday"
},
"endHour": {
"type": "number",
"description": "end hour of the window, 0-23"
},
"startDay": {
"type": "number",
"description": "0 = Sunday … 6 = Saturday"
},
"startHour": {
"type": "number",
"description": "0-23, in the ad group’s time zone"
}
}
},
"description": "weekly dayparting windows — omit to run all week"
},
"bidAmount": {
"type": "number",
"description": "bid in the ad account’s currency — required for paid bid types; ask reddit_ads_bid_suggestion"
},
"startTime": {
"type": "string",
"description": "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"
},
"targeting": {
"type": "object",
"properties": {
"gender": {
"type": "string",
"description": "MALE or FEMALE — omit for all"
},
"devices": {
"type": "array",
"items": {},
"description": "device targets (from search_reddit_ads_targeting kind devices)"
},
"carriers": {
"type": "array",
"items": {
"type": "string"
},
"description": "carrier ids such as VERIZON_US (from search_reddit_ads_targeting kind carriers)"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "keywords to target"
},
"interests": {
"type": "array",
"items": {
"type": "string"
},
"description": "interest ids like [\"pets_v3\"]"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "e.g. [\"EN\"]"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "FEED and/or COMMENTS_PAGE"
},
"platforms": {
"type": "array",
"items": {
"type": "string"
},
"description": "ALL / DESKTOP / MOBILE_NATIVE / MOBILE_WEB — at least one mobile type is expected"
},
"viewModes": {
"type": "array",
"items": {
"type": "string"
},
"description": "ALL / CARD / CLASSIC / COMPACT / IMMERSIVE"
},
"communities": {
"type": "array",
"items": {
"type": "string"
},
"description": "bare subreddit NAMES, e.g. [\"running\"] — not t5_ ids, not \"r/running\""
},
"geolocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "country codes like [\"US\"], or ids from search_reddit_ads_targeting"
},
"expandTargeting": {
"type": "boolean",
"description": "let Reddit widen the audience automatically"
},
"excludedKeywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "keywords to exclude"
},
"customAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "custom audience ids to include (from list_reddit_ads_audiences)"
},
"excludedInterests": {
"type": "array",
"items": {
"type": "string"
},
"description": "interest ids to exclude, e.g. pets_v3"
},
"excludedCommunities": {
"type": "array",
"items": {
"type": "string"
},
"description": "subreddit names to exclude"
},
"excludedGeolocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "country codes or location ids to exclude"
},
"suppressionEventTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "[\"ALL_FEATURES\"] to stop showing this to people who already converted — that is the only value Reddit accepts"
},
"excludedCustomAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "custom audience ids to exclude"
}
},
"description": "targeting block; every key optional (communities, geolocations, interests, keywords, audiences, devices)"
},
"campaignId": {
"type": "string",
"description": "the campaign this ad group belongs to"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"bidStrategy": {
"enum": [
"BIDLESS",
"MANUAL_BIDDING",
"MAXIMIZE_VOLUME",
"TARGET_CPX"
],
"type": "string",
"description": "required — MANUAL_BIDDING needs bidAmount; under a CBO campaign it is taken from the campaign"
},
"productSetId": {
"type": "string",
"description": "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": {
"enum": [
"DYNAMIC",
"STATIC"
],
"type": "string",
"description": "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"
},
"savedAudienceId": {
"type": "string",
"description": "reuse a saved audience instead of spelling targeting out — from list_reddit_ads_saved_audiences"
},
"optimizationGoal": {
"type": "string",
"description": "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"
},
"conversionPixelId": {
"type": "string",
"description": "only needed when the ad account has more than one pixel"
},
"viewThroughConversionType": {
"enum": [
"SEVEN_DAY_CLICKS",
"SEVEN_DAY_CLICKS_ONE_DAY_VIEW"
],
"type": "string",
"description": "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"
}
}
}| endTime |
| string |
| CBO only: ISO 8601, required for LIFETIME_SPEND |
| goalType | string | CBO only, default DAILY_SPEND; LIFETIME_SPEND needs endTime |
| schedule | array | CBO only: weekly delivery windows (day 0 = Sunday, hour 0-23) — replaces every ad group’s schedule |
| 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 |
| objective | string | default CLICKS — which is what Reddit calls website traffic |
| startTime | string | CBO only: ISO 8601 |
| 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 |
| adAccountId | string | Reddit ad account id (a2_…) — omit when only one is shared |
| bidStrategy | string | CBO only (MANUAL_BIDDING is ad-group only) |
| invoiceLabel | string | agency reconciliation label on the invoice, ≤80 characters — Reddit ignores it on credit-card-funded campaigns, and the reply says so |
| spendCapCents | number | lifetime spend ceiling for the whole campaign, in minor units of the ad account’s currency |
| optimizationGoal | string | CBO only: e.g. PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT, CLICKS. Cannot be changed later |
| conversionPixelId | string | CBO only: needed when the ad account has more than one pixel |
| fundingInstrumentId | string | bill this campaign to a specific Reddit funding instrument |
| specialAdCategories | array | HOUSING_EMPLOYMENT_CREDIT for housing, employment or credit ads — Reddit then restricts their targeting. Cannot be changed after creation |
| viewThroughConversionType | string | CBO only: the conversion attribution window. Cannot be changed later |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"appId": {
"type": "string",
"description": "APP_INSTALLS only: the App Store id (e.g. 1064216828) or Google Play package (e.g. com.reddit.frontpage)"
},
"budget": {
"type": "number",
"description": "CBO only: the campaign budget in the ad account’s currency, paired with goalType"
},
"bidType": {
"enum": [
"CPC",
"CPM",
"CPV6",
"CPV15"
],
"type": "string",
"description": "CBO only, must fit the objective — CLICKS takes CPC"
},
"endTime": {
"type": "string",
"description": "CBO only: ISO 8601, required for LIFETIME_SPEND"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "CBO only, default DAILY_SPEND; LIFETIME_SPEND needs endTime"
},
"schedule": {
"type": "array",
"items": {
"type": "object",
"required": [
"startDay",
"endDay",
"startHour",
"endHour"
],
"properties": {
"endDay": {
"type": "number",
"description": "last weekday of the window, 0 = Sunday"
},
"endHour": {
"type": "number",
"description": "end hour of the window, 0-23"
},
"startDay": {
"type": "number",
"description": "first weekday of the window, 0 = Sunday"
},
"startHour": {
"type": "number",
"description": "start hour of the window, 0-23"
}
}
},
"description": "CBO only: weekly delivery windows (day 0 = Sunday, hour 0-23) — replaces every ad group’s schedule"
},
"bidAmount": {
"type": "number",
"description": "CBO only: target cost in the ad account’s currency — required for TARGET_CPX; when set here no ad group may set its own"
},
"objective": {
"enum": [
"APP_INSTALLS",
"BRAND_AWARENESS",
"CATALOG_SALES",
"CLICKS",
"CONVERSIONS",
"IMPRESSIONS",
"LEAD_GENERATION",
"SALES",
"VIDEO_VIEWABLE_IMPRESSIONS"
],
"type": "string",
"description": "default CLICKS — which is what Reddit calls website traffic"
},
"startTime": {
"type": "string",
"description": "CBO only: ISO 8601"
},
"useCatalog": {
"type": "boolean",
"description": "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"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_…) — omit when only one is shared"
},
"bidStrategy": {
"enum": [
"BIDLESS",
"MAXIMIZE_VOLUME",
"TARGET_CPX"
],
"type": "string",
"description": "CBO only (MANUAL_BIDDING is ad-group only)"
},
"invoiceLabel": {
"type": "string",
"description": "agency reconciliation label on the invoice, ≤80 characters — Reddit ignores it on credit-card-funded campaigns, and the reply says so"
},
"spendCapCents": {
"type": "number",
"description": "lifetime spend ceiling for the whole campaign, in minor units of the ad account’s currency"
},
"optimizationGoal": {
"type": "string",
"description": "CBO only: e.g. PURCHASE, SIGN_UP, LEAD, ADD_TO_CART, PAGE_VISIT, CLICKS. Cannot be changed later"
},
"conversionPixelId": {
"type": "string",
"description": "CBO only: needed when the ad account has more than one pixel"
},
"fundingInstrumentId": {
"type": "string",
"description": "bill this campaign to a specific Reddit funding instrument"
},
"specialAdCategories": {
"type": "array",
"items": {
"enum": [
"HOUSING_EMPLOYMENT_CREDIT",
"NONE"
],
"type": "string"
},
"description": "HOUSING_EMPLOYMENT_CREDIT for housing, employment or credit ads — Reddit then restricts their targeting. Cannot be changed after creation"
},
"viewThroughConversionType": {
"enum": [
"SEVEN_DAY_CLICKS",
"SEVEN_DAY_CLICKS_ONE_DAY_VIEW"
],
"type": "string",
"description": "CBO only: the conversion attribution window. Cannot be changed later"
},
"isCampaignBudgetOptimization": {
"type": "boolean",
"description": "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"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"defaultCurrency"
],
"properties": {
"name": {
"type": "string",
"description": "catalog name"
},
"pixelIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Reddit pixel ids from list_reddit_ads_pixels to match products against; each must be on the shared ad account"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"defaultCurrency": {
"enum": [
"USD",
"GBP",
"CAD",
"EUR",
"AUD",
"JPY",
"CHF",
"NZD",
"SEK",
"NOK",
"BRL"
],
"type": "string",
"description": "the currency your product prices are in — fixed once created"
},
"defaultLanguage": {
"enum": [
"en",
"de",
"es",
"fr",
"it",
"pt"
],
"type": "string",
"description": "default en — fixed once created"
}
}
}| at least one |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| privacyLink* | string | full https:// URL to your privacy policy — Reddit requires it |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"prompt",
"privacyLink",
"questions"
],
"properties": {
"name": {
"type": "string",
"description": "internal name — redditors do not see it"
},
"prompt": {
"type": "string",
"description": "the line shown above the form telling people what they are signing up for"
},
"questions": {
"type": "array",
"items": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"enum": [
"EMAIL",
"FIRST_NAME",
"LAST_NAME",
"PHONE_NUMBER",
"POSTAL_CODE",
"JOB_TITLE",
"COMPANY",
"COMPANY_EMAIL"
],
"type": "string",
"description": "contact field to ask: EMAIL, FIRST_NAME, LAST_NAME, PHONE_NUMBER, POSTAL_CODE, JOB_TITLE, COMPANY or COMPANY_EMAIL"
},
"required": {
"type": "boolean",
"description": "default true"
}
}
},
"description": "at least one"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"privacyLink": {
"type": "string",
"description": "full https:// URL to your privacy policy — Reddit requires it"
}
}
}| bidType | string | default CPC for CLICKS; required for the other objectives |
| endTime | string | ISO 8601 end time; required for LIFETIME_SPEND |
| assetIds* | array | 5 to 45 ACTIVE creative-library asset ids: at least 3 HEADLINE and 2 IMAGE or VIDEO |
| goalType | string | default DAILY_SPEND; LIFETIME_SPEND needs endTime |
| schedule | array | weekly windows, day 0 = Sunday |
| spendCap | number | lifetime spend ceiling in the ad account’s currency; DAILY_SPEND only, at or above the daily budget |
| bidAmount | number | target cost per result in the ad account’s currency; required for TARGET_CPX |
| objective | string | default CLICKS; CONVERSIONS is the legacy name of SALES |
| startTime* | string | ISO 8601, e.g. 2026-09-20T00:00:00Z |
| targeting* | object | SEED signals: at least one of communities, interests, keywords or customAudienceIds; geolocations and the excluded lists are hard controls |
| displayUrl | string | URL shown on the ad; must be on the same domain as destinationUrl |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| adGroupName | string | name of the automated ad group |
| bidStrategy | string | default BIDLESS |
| invoiceLabel | string | up to 80 characters on the invoice; ignored on card-funded accounts |
| destinationUrl* | string | the https landing page |
| redditProfileId* | string | the t2_ profile the template ad runs as, from list_reddit_ads_profiles |
| 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 |
| conversionPixelId | string | only needed when the ad account has more than one pixel |
| supplementaryText | string | up to 100 characters |
| thumbnailAssetIds | array | up to 10 |
| fundingInstrumentId | string | funding instrument id from list_reddit_ads_funding_instruments |
| specialAdCategories | array | cannot change after publishing |
| userGeneratedContent | string | let Reddit add user-generated content enhancements |
| viewThroughConversionType | string | conversion attribution window: SEVEN_DAY_CLICKS or SEVEN_DAY_CLICKS_ONE_DAY_VIEW |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"budget",
"startTime",
"targeting",
"redditProfileId",
"assetIds",
"destinationUrl"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name, 3 to 500 characters"
},
"appId": {
"type": "string",
"description": "App Store or Google Play id; required for APP_INSTALLS"
},
"adName": {
"type": "string",
"description": "name of the template ad"
},
"budget": {
"type": "number",
"description": "campaign budget in the ad account’s currency (a Max budget lives on the campaign)"
},
"bidType": {
"enum": [
"CPC",
"CPM",
"CPV6",
"CPV15"
],
"type": "string",
"description": "default CPC for CLICKS; required for the other objectives"
},
"endTime": {
"type": "string",
"description": "ISO 8601 end time; required for LIFETIME_SPEND"
},
"assetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "5 to 45 ACTIVE creative-library asset ids: at least 3 HEADLINE and 2 IMAGE or VIDEO"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "default DAILY_SPEND; LIFETIME_SPEND needs endTime"
},
"schedule": {
"type": "array",
"items": {
"type": "object",
"required": [
"startDay",
"endDay",
"startHour",
"endHour"
],
"properties": {
"endDay": {
"type": "number",
"description": "last weekday of the window, 0 = Sunday"
},
"endHour": {
"type": "number",
"description": "end hour of the window, 0-23"
},
"startDay": {
"type": "number",
"description": "first weekday of the window, 0 = Sunday"
},
"startHour": {
"type": "number",
"description": "start hour of the window, 0-23"
}
}
},
"description": "weekly windows, day 0 = Sunday"
},
"spendCap": {
"type": "number",
"description": "lifetime spend ceiling in the ad account’s currency; DAILY_SPEND only, at or above the daily budget"
},
"bidAmount": {
"type": "number",
"description": "target cost per result in the ad account’s currency; required for TARGET_CPX"
},
"objective": {
"enum": [
"CLICKS",
"SALES",
"LEAD_GENERATION",
"APP_INSTALLS",
"CONVERSIONS"
],
"type": "string",
"description": "default CLICKS; CONVERSIONS is the legacy name of SALES"
},
"startTime": {
"type": "string",
"description": "ISO 8601, e.g. 2026-09-20T00:00:00Z"
},
"targeting": {
"type": "object",
"properties": {
"gender": {
"type": "string",
"description": "MALE or FEMALE; omit for all"
},
"devices": {
"type": "array",
"items": {},
"description": "APP_INSTALLS needs one mobile OS matching the app"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "seed keywords for the automated ad group"
},
"interests": {
"type": "array",
"items": {
"type": "string"
},
"description": "Reddit interest ids, e.g. pets_v3 (from search_reddit_ads_targeting)"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "language codes, e.g. EN"
},
"communities": {
"type": "array",
"items": {
"type": "string"
},
"description": "bare subreddit names, e.g. [\"marketing\"]"
},
"geolocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "country codes like [\"US\"]: a hard control"
},
"expandTargeting": {
"type": "boolean",
"description": "true lets Reddit widen the audience automatically"
},
"excludedKeywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "keywords to exclude"
},
"customAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "custom audience ids to include"
},
"excludedCommunities": {
"type": "array",
"items": {
"type": "string"
},
"description": "subreddit names to exclude"
},
"excludedGeolocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "country codes or location ids to exclude"
},
"excludedCustomAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "custom audience ids to exclude"
}
},
"description": "SEED signals: at least one of communities, interests, keywords or customAudienceIds; geolocations and the excluded lists are hard controls"
},
"displayUrl": {
"type": "string",
"description": "URL shown on the ad; must be on the same domain as destinationUrl"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"adGroupName": {
"type": "string",
"description": "name of the automated ad group"
},
"bidStrategy": {
"enum": [
"BIDLESS",
"MAXIMIZE_VOLUME",
"TARGET_CPX"
],
"type": "string",
"description": "default BIDLESS"
},
"invoiceLabel": {
"type": "string",
"description": "up to 80 characters on the invoice; ignored on card-funded accounts"
},
"destinationUrl": {
"type": "string",
"description": "the https landing page"
},
"redditProfileId": {
"type": "string",
"description": "the t2_ profile the template ad runs as, from list_reddit_ads_profiles"
},
"optimizationGoal": {
"type": "string",
"description": "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"
},
"conversionPixelId": {
"type": "string",
"description": "only needed when the ad account has more than one pixel"
},
"supplementaryText": {
"type": "string",
"description": "up to 100 characters"
},
"thumbnailAssetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 10"
},
"fundingInstrumentId": {
"type": "string",
"description": "funding instrument id from list_reddit_ads_funding_instruments"
},
"specialAdCategories": {
"type": "array",
"items": {
"enum": [
"HOUSING_EMPLOYMENT_CREDIT",
"NONE"
],
"type": "string"
},
"description": "cannot change after publishing"
},
"userGeneratedContent": {
"enum": [
"OPT_IN",
"OPT_OUT"
],
"type": "string",
"description": "let Reddit add user-generated content enhancements"
},
"viewThroughConversionType": {
"enum": [
"SEVEN_DAY_CLICKS",
"SEVEN_DAY_CLICKS_ONE_DAY_VIEW"
],
"type": "string",
"description": "conversion attribution window: SEVEN_DAY_CLICKS or SEVEN_DAY_CLICKS_ONE_DAY_VIEW"
}
}
}| string |
| the post title — this is the ad’s headline |
| textFormat | string | 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. |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| 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 |
| redditProfileId* | string | the Reddit profile id (t2_…) to publish as — from list_reddit_ads_profiles |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"redditProfileId",
"headline"
],
"properties": {
"body": {
"type": "string",
"description": "body copy: the text of a TEXT post, or text shown with an IMAGE, VIDEO or CAROUSEL post. Up to 40,000 characters."
},
"type": {
"enum": [
"TEXT",
"IMAGE",
"VIDEO",
"CAROUSEL"
],
"type": "string",
"description": "default TEXT"
},
"jobId": {
"type": "string",
"description": "PICK UP a post Reddit was still building: the jobId from an earlier pending answer. Creates nothing new; every other field is ignored."
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"caption": {
"type": "string",
"description": "carousel card caption"
},
"mediaUrl": {
"type": "string",
"description": "PUBLIC url of the image/video — Reddit fetches it, minimum 140×140"
},
"displayUrl": {
"type": "string",
"description": "shown instead of the destination; must be the same domain"
},
"callToAction": {
"type": "string",
"description": "e.g. \"Learn More\", \"Shop Now\" — exact human-readable strings"
},
"destinationUrl": {
"type": "string",
"description": "where a click goes — required for image and carousel posts"
}
}
},
"description": "one entry for IMAGE/VIDEO, up to 6 for CAROUSEL"
},
"headline": {
"type": "string",
"description": "the post title — this is the ad’s headline"
},
"textFormat": {
"enum": [
"PLAIN_TEXT",
"RICH_TEXT_JSON"
],
"type": "string",
"description": "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."
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"callToAction": {
"type": "string",
"description": "applies to every media entry that has none"
},
"thumbnailUrl": {
"type": "string",
"description": "required for VIDEO posts"
},
"allowComments": {
"type": "boolean",
"description": "Reddit ads can carry a public comment thread — decide deliberately"
},
"redditProfileId": {
"type": "string",
"description": "the Reddit profile id (t2_…) to publish as — from list_reddit_ads_profiles"
}
}
}| catalog to import the feed into |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId",
"url"
],
"properties": {
"url": {
"type": "string",
"description": "the product file Reddit fetches"
},
"mode": {
"enum": [
"REPLACE",
"UPDATE"
],
"type": "string",
"description": "default REPLACE — the file is the whole catalog"
},
"name": {
"type": "string",
"description": "feed name, for your reference"
},
"password": {
"type": "string",
"description": "password for the feed URL; sent to Reddit and never echoed back"
},
"schedule": {
"type": "object",
"properties": {
"hour": {
"type": "number",
"description": "0-23"
},
"minute": {
"type": "number",
"description": "0-59"
},
"interval": {
"enum": [
"HOURLY",
"DAILY",
"WEEKLY",
"MONTHLY"
],
"type": "string",
"description": "HOURLY, DAILY, WEEKLY or MONTHLY re-fetch cadence"
},
"isPaused": {
"type": "boolean",
"description": "true pauses scheduled re-fetching"
},
"timezone": {
"type": "string",
"description": "IANA zone, e.g. America/New_York"
},
"dayOfWeek": {
"enum": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY",
"SATURDAY",
"SUNDAY"
],
"type": "string",
"description": "required for WEEKLY"
},
"dayOfMonth": {
"type": "number",
"description": "1-31, required for MONTHLY"
},
"intervalCount": {
"type": "number",
"description": "every N intervals, default 1"
}
},
"description": "how often Reddit re-fetches the file; omit for a one-off"
},
"username": {
"type": "string",
"description": "only if the url needs a login"
},
"catalogId": {
"type": "string",
"description": "catalog to import the feed into"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adSquadId",
"name",
"creativeId"
],
"properties": {
"name": {
"type": "string",
"description": "max 375 characters"
},
"type": {
"type": "string",
"description": "omit it — Hermoso reads the creative and picks the ad type Snapchat pairs with it (WEB_VIEW -> REMOTE_WEBPAGE, LEAD_GENERATION -> LEAD_GENERATION, SNAP_AD -> SNAP_AD). Pass one only to override."
},
"adSquadId": {
"type": "string",
"description": "the ad squad this ad belongs to"
},
"creativeId": {
"type": "string",
"description": "from upload_snapchat_ads_creative — NOT a mediaId"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
}
}
}| metros | array | metro/DMA ids inside the one country named in countries — resolve with kind:"metro" |
| minAge | string | e.g. "18" |
| osType | string | device OS — resolve with search_snapchat_ads_targeting(kind:"os_type") |
| endTime | string | ISO 8601 |
| pixelId | string | the Snap Pixel a PIXEL_* optimizationGoal optimises toward — from list_snapchat_ads_pixels. Omit it and the ad account’s ONLY active pixel is used and named in the reply; an account with none or several is refused by name, never guessed. The pixel Snapchat stored is read back. |
| regions | array | region/state ids INSIDE the one country named in countries — resolve with search_snapchat_ads_targeting(kind:"region", countryCode:"ca"). Refused if more than one country is given, because a region id belongs to a country. |
| bidMicro | number | ONLY if already converted; never alongside bid |
| audiences | array | audience ids to TARGET — customer lists, lookalikes, pixel audiences (list_snapchat_ads_audiences); only READY ones deliver |
| countries* | array | REQUIRED — two-letter country codes, e.g. ["us"]. Snapchat refuses an ad squad with no geo targeting. |
| interests | array | Snapchat interest category ids such as ["SLC_1"] — resolve with search_snapchat_ads_targeting(kind:"interest", taxonomy:"scls"). Ids are NOT interchangeable between taxonomies. |
| languages | array | language codes such as ["en","es"] — resolve them with search_snapchat_ads_targeting(kind:"language") |
| startTime | string | ISO 8601 |
| campaignId* | string | the campaign this ad squad belongs to |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| bidStrategy | string | default AUTO_BID — Snapchat picks the bid, and no bid field is needed. ONLY AUTO_BID and LOWEST_COST_WITH_MAX_BID actually work: Snapchat deprecated MIN_ROAS (with roas_value_micro) on 10 February 2025 and its ad-squads reference lists BOTH MIN_ROAS and TARGET_COST as not available, so either is refused by name before anything is created. They stay in the enum because they are still in Snapchat published enum — the refusal names the vendor, not your input. |
| dailyBudget | number | in the ad account’s own currency — 50 means fifty. Hermoso converts to micro. Minimum 5. |
| postalCodes | array | postal codes inside the one country named in countries |
| billingEvent | string | IMPRESSION — the only value Snapchat documents |
| lifetimeBudget | number | in the ad account’s own currency — an alternative to a daily budget |
| placementConfig | string | default AUTOMATIC — Snapchat places the ad across its surfaces |
| dailyBudgetMicro | number | ONLY if you have already multiplied by 1,000,000. Passing this AND dailyBudget is refused. |
| excludeAudiences | array | audience ids to EXCLUDE, e.g. existing customers |
| optimizationGoal | string | what Snapchat optimises delivery toward — default IMPRESSIONS. Others include SWIPES, VIDEO_VIEWS, APP_INSTALLS, PIXEL_PURCHASE, LEAD_FORM_SUBMISSIONS. |
| regulatedContent | boolean | declare regulated content (alcohol, gambling and the like) |
| lifetimeBudgetMicro | number | lifetime budget in micro-currency (1,000,000 = 1 unit); only if already converted, never with lifetimeBudget |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name",
"countries"
],
"properties": {
"bid": {
"type": "number",
"description": "in the ad account’s own currency — required unless bidStrategy is AUTO_BID"
},
"name": {
"type": "string",
"description": "max 375 characters"
},
"type": {
"enum": [
"SNAP_ADS",
"LENS",
"FILTER"
],
"type": "string",
"description": "default SNAP_ADS"
},
"gender": {
"enum": [
"MALE",
"FEMALE",
"OTHER"
],
"type": "string",
"description": "leave unset to reach everyone — an absent gender is no gender restriction, not a default"
},
"maxAge": {
"type": "string",
"description": "maximum target age as a string, e.g. '65'; omit for no upper bound"
},
"metros": {
"type": "array",
"items": {
"type": "string"
},
"description": "metro/DMA ids inside the one country named in countries — resolve with kind:\"metro\""
},
"minAge": {
"type": "string",
"description": "e.g. \"18\""
},
"osType": {
"enum": [
"iOS",
"ANDROID",
"WEB"
],
"type": "string",
"description": "device OS — resolve with search_snapchat_ads_targeting(kind:\"os_type\")"
},
"endTime": {
"type": "string",
"description": "ISO 8601"
},
"pixelId": {
"type": "string",
"description": "the Snap Pixel a PIXEL_* optimizationGoal optimises toward — from list_snapchat_ads_pixels. Omit it and the ad account’s ONLY active pixel is used and named in the reply; an account with none or several is refused by name, never guessed. The pixel Snapchat stored is read back."
},
"regions": {
"type": "array",
"items": {
"type": "string"
},
"description": "region/state ids INSIDE the one country named in countries — resolve with search_snapchat_ads_targeting(kind:\"region\", countryCode:\"ca\"). Refused if more than one country is given, because a region id belongs to a country."
},
"bidMicro": {
"type": "number",
"description": "ONLY if already converted; never alongside bid"
},
"audiences": {
"type": "array",
"items": {
"type": "string"
},
"description": "audience ids to TARGET — customer lists, lookalikes, pixel audiences (list_snapchat_ads_audiences); only READY ones deliver"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED — two-letter country codes, e.g. [\"us\"]. Snapchat refuses an ad squad with no geo targeting."
},
"interests": {
"type": "array",
"items": {
"type": "string"
},
"description": "Snapchat interest category ids such as [\"SLC_1\"] — resolve with search_snapchat_ads_targeting(kind:\"interest\", taxonomy:\"scls\"). Ids are NOT interchangeable between taxonomies."
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "language codes such as [\"en\",\"es\"] — resolve them with search_snapchat_ads_targeting(kind:\"language\")"
},
"startTime": {
"type": "string",
"description": "ISO 8601"
},
"campaignId": {
"type": "string",
"description": "the campaign this ad squad belongs to"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"bidStrategy": {
"enum": [
"AUTO_BID",
"LOWEST_COST_WITH_MAX_BID",
"TARGET_COST",
"MIN_ROAS"
],
"type": "string",
"description": "default AUTO_BID — Snapchat picks the bid, and no bid field is needed. ONLY AUTO_BID and LOWEST_COST_WITH_MAX_BID actually work: Snapchat deprecated MIN_ROAS (with roas_value_micro) on 10 February 2025 and its ad-squads reference lists BOTH MIN_ROAS and TARGET_COST as not available, so either is refused by name before anything is created. They stay in the enum because they are still in Snapchat published enum — the refusal names the vendor, not your input."
},
"dailyBudget": {
"type": "number",
"description": "in the ad account’s own currency — 50 means fifty. Hermoso converts to micro. Minimum 5."
},
"postalCodes": {
"type": "array",
"items": {
"type": "string"
},
"description": "postal codes inside the one country named in countries"
},
"billingEvent": {
"type": "string",
"description": "IMPRESSION — the only value Snapchat documents"
},
"lifetimeBudget": {
"type": "number",
"description": "in the ad account’s own currency — an alternative to a daily budget"
},
"placementConfig": {
"enum": [
"AUTOMATIC",
"CUSTOM"
],
"type": "string",
"description": "default AUTOMATIC — Snapchat places the ad across its surfaces"
},
"dailyBudgetMicro": {
"type": "number",
"description": "ONLY if you have already multiplied by 1,000,000. Passing this AND dailyBudget is refused."
},
"excludeAudiences": {
"type": "array",
"items": {
"type": "string"
},
"description": "audience ids to EXCLUDE, e.g. existing customers"
},
"optimizationGoal": {
"type": "string",
"description": "what Snapchat optimises delivery toward — default IMPRESSIONS. Others include SWIPES, VIDEO_VIEWS, APP_INSTALLS, PIXEL_PURCHASE, LEAD_FORM_SUBMISSIONS."
},
"regulatedContent": {
"type": "boolean",
"description": "declare regulated content (alcohol, gambling and the like)"
},
"lifetimeBudgetMicro": {
"type": "number",
"description": "lifetime budget in micro-currency (1,000,000 = 1 unit); only if already converted, never with lifetimeBudget"
}
}
}| BALANCE (default), SIMILARITY or REACH; LOOKALIKE audiences only |
| seedSegmentId | string | LOOKALIKE: the customer-list audience to model |
| retentionInDays | number | FIRST_PARTY default lifetime (9999); LOOKALIKE max 180 (default 180); PIXEL required, max 395 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "audience name in Snapchat Ads Manager"
},
"events": {
"type": "array",
"items": {
"type": "string"
},
"description": "PIXEL: the events that put someone in it, e.g. [\"PURCHASE\",\"START_CHECKOUT\"]"
},
"pixelId": {
"type": "string",
"description": "PIXEL: from list_snapchat_ads_pixels"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "LOOKALIKE: two-letter codes, e.g. [\"US\"]"
},
"sourceType": {
"enum": [
"FIRST_PARTY",
"LOOKALIKE",
"PIXEL"
],
"type": "string",
"description": "default FIRST_PARTY"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"description": {
"type": "string",
"description": "audience description; optional"
},
"lookalikeType": {
"enum": [
"BALANCE",
"SIMILARITY",
"REACH"
],
"type": "string",
"description": "BALANCE (default), SIMILARITY or REACH; LOOKALIKE audiences only"
},
"seedSegmentId": {
"type": "string",
"description": "LOOKALIKE: the customer-list audience to model"
},
"retentionInDays": {
"type": "number",
"description": "FIRST_PARTY default lifetime (9999); LOOKALIKE max 180 (default 180); PIXEL required, max 395"
}
}
}| from list_snapchat_ads_accounts — omit only when exactly one is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the campaign name in Snapchat Ads Manager — max 375 characters"
},
"endTime": {
"type": "string",
"description": "ISO 8601"
},
"buyModel": {
"enum": [
"AUCTION",
"RESERVED"
],
"type": "string",
"description": "default AUCTION. RESERVED is Reach & Frequency booking and has its own contract."
},
"objective": {
"type": "string",
"description": "AWARENESS_AND_ENGAGEMENT, SALES, TRAFFIC, APP_PROMOTION or LEADS. A legacy name is refused by name."
},
"startTime": {
"type": "string",
"description": "ISO 8601 — defaults to now, which is harmless because the campaign is paused"
},
"adAccountId": {
"type": "string",
"description": "from list_snapchat_ads_accounts — omit only when exactly one is reachable"
}
}
}| description* | string | max 180 characters, shown above the questions |
| bannerMediaId | string | an IMAGE media id already on the ad account; 75:23, 750x230 to 1875x575, 5MB |
| privacyPolicyUrl | string | linked on the confirmation screen |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"title",
"description",
"fields",
"endPage"
],
"properties": {
"name": {
"type": "string",
"description": "the form name in Ads Manager"
},
"legal": {
"type": "object",
"required": [
"title",
"description"
],
"properties": {
"title": {
"type": "string",
"description": "legal disclosure heading shown on the form, max 35 characters"
},
"consents": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string"
},
"required": {
"type": "boolean"
}
}
}
]
},
"description": "up to 2 consent checkboxes: plain text, or {text, required}"
},
"description": {
"type": "string",
"description": "legal disclosure body text, max 80 characters"
}
},
"description": "legal disclosure: title max 35, description max 80, up to 2 consent checkboxes"
},
"title": {
"type": "string",
"description": "max 25 characters, shown at the top of the form"
},
"fields": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"kind": {
"enum": [
"TEXT",
"DATE",
"MULTIPLE_CHOICE_SINGLE_SELECTION",
"MULTIPLE_CHOICE_MULTI_SELECTION"
],
"type": "string"
},
"type": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string"
},
"preferred": {
"type": "boolean"
}
}
}
]
}
},
"question": {
"type": "string"
}
}
}
]
},
"description": "the questions, in order: standard types as strings (FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, ADDRESS, POSTAL_CODE, BIRTHDAY_DATE, JOB_TITLE, COMPANY_NAME) and custom questions as {question, kind, options}"
},
"endPage": {
"type": "object",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "where its button goes"
},
"headline": {
"type": "string",
"description": "defaults to the form title"
},
"description": {
"type": "string",
"description": "defaults to the form description"
},
"callToAction": {
"enum": [
"VIEW_WEBSITE",
"BOOK_NOW",
"LEARN_MORE",
"DONATE",
"SPECIAL_OFFER",
"SCHEDULE_NOW",
"BUY_TICKETS",
"TEST_DRIVE",
"APPLY_NOW",
"GET_COUPON",
"CLAIM_SAMPLE",
"FREE_TRIAL"
],
"type": "string",
"description": "default VIEW_WEBSITE — the end page has its own list, not the creative one"
}
},
"description": "REQUIRED by Snapchat (E25022): the screen shown after someone submits"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"description": {
"type": "string",
"description": "max 180 characters, shown above the questions"
},
"bannerMediaId": {
"type": "string",
"description": "an IMAGE media id already on the ad account; 75:23, 750x230 to 1875x575, 5MB"
},
"privacyPolicyUrl": {
"type": "string",
"description": "linked on the confirmation screen"
}
}
}| string |
| YYYY-MM-DD the code stops working |
| percentOff | number | percent discount, 1 to 100; use this or amountOff |
| firstTimeOnly | boolean | the code works only on a customer's first purchase |
| minimumAmount | number | minimum order in MAJOR units for the code |
| maxRedemptions | number | uses of the COUPON across every code |
| durationInMonths | number | months the discount applies, with duration repeating |
| codeMaxRedemptions | number | uses of this CODE |
| minimumAmountCurrency | string | currency for the code's minimumAmount, e.g. usd |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "custom coupon id; Stripe generates one if omitted"
},
"code": {
"type": "string",
"description": "the customer-typable promotion code, e.g. SUMMER20 — letters, digits and dashes only; refused (never altered) otherwise"
},
"name": {
"type": "string",
"description": "display name for the coupon, for your records"
},
"confirm": {
"type": "boolean",
"description": "true on a live key, after the user approves the preview"
},
"currency": {
"type": "string",
"description": "3-letter currency code; required with amountOff"
},
"duration": {
"type": "string",
"description": "once (default), repeating or forever"
},
"amountOff": {
"type": "number",
"description": "fixed discount in major units, e.g. 5 = 5.00; needs currency"
},
"expiresAt": {
"type": "string",
"description": "YYYY-MM-DD the code stops working"
},
"percentOff": {
"type": "number",
"description": "percent discount, 1 to 100; use this or amountOff"
},
"firstTimeOnly": {
"type": "boolean",
"description": "the code works only on a customer's first purchase"
},
"minimumAmount": {
"type": "number",
"description": "minimum order in MAJOR units for the code"
},
"maxRedemptions": {
"type": "number",
"description": "uses of the COUPON across every code"
},
"durationInMonths": {
"type": "number",
"description": "months the discount applies, with duration repeating"
},
"codeMaxRedemptions": {
"type": "number",
"description": "uses of this CODE"
},
"minimumAmountCurrency": {
"type": "string",
"description": "currency for the code's minimumAmount, e.g. usd"
}
}
}| afterCompletionUrl | string | URL the buyer is sent to after paying |
| allowPromotionCodes | boolean | show the promotion-code box at checkout |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "UTM term on the tagged URL; letters, digits, dashes and underscores, up to 150"
},
"medium": {
"type": "string",
"description": "UTM medium on the tagged URL; letters, digits, dashes and underscores, up to 150"
},
"source": {
"type": "string",
"description": "UTM source on the tagged URL; letters, digits, dashes and underscores, up to 150"
},
"content": {
"type": "string",
"description": "UTM content on the tagged URL; letters, digits, dashes and underscores, up to 150"
},
"campaign": {
"type": "string",
"description": "UTM campaign on the tagged URL; letters, digits, dashes and underscores, up to 150"
}
},
"description": "UTM values on the taggedUrl (letters, digits, dashes, underscores, up to 150 each); Stripe passes them to the after-payment redirect only"
},
"confirm": {
"type": "boolean",
"description": "true on a live key, after the user approves the preview"
},
"priceId": {
"type": "string",
"description": "one price — the shorthand for a single line item"
},
"metadata": {
"type": "object",
"description": "key/value metadata stored on the payment link",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"quantity": {
"type": "number",
"description": "quantity of priceId, e.g. 2"
},
"lineItems": {
"type": "array",
"items": {
"type": "object",
"required": [
"priceId"
],
"properties": {
"priceId": {
"type": "string",
"description": "price id, price_..."
},
"quantity": {
"type": "number",
"description": "quantity of this price in the link"
}
}
},
"description": "several prices at once; use instead of priceId"
},
"referenceId": {
"type": "string",
"description": "letters, digits, dashes, underscores (up to 200) — tags the returned taggedUrl for one ad; Stripe stores it on each purchase"
},
"afterCompletionUrl": {
"type": "string",
"description": "URL the buyer is sent to after paying"
},
"allowPromotionCodes": {
"type": "boolean",
"description": "show the promotion-code box at checkout"
}
}
}| string |
| product description shown to customers in Stripe |
| intervalCount | number | intervals between charges, e.g. 3 with month for quarterly |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"amount",
"currency"
],
"properties": {
"name": {
"type": "string",
"description": "product name"
},
"amount": {
"type": "number",
"description": "MAJOR units — 19 or 19.00 is $19.00, never 1900"
},
"confirm": {
"type": "boolean",
"description": "true on a live key, after the user approves the preview"
},
"currency": {
"type": "string",
"description": "3-letter ISO currency code, e.g. usd"
},
"interval": {
"type": "string",
"description": "recurring interval: day, week, month or year; omit for a one-time price"
},
"metadata": {
"type": "object",
"description": "key/value metadata stored on the product",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"nickname": {
"type": "string",
"description": "internal label for the default price; not shown to customers"
},
"description": {
"type": "string",
"description": "product description shown to customers in Stripe"
},
"intervalCount": {
"type": "number",
"description": "intervals between charges, e.g. 3 with month for quarterly"
}
}
}| minimumAmount |
| number |
| MAJOR units |
| maxRedemptions | number | maximum uses of this code |
| minimumAmountCurrency | string | 3-letter currency code for minimumAmount |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"couponId"
],
"properties": {
"code": {
"type": "string",
"description": "customer-typable code, letters, digits and dashes; Stripe generates one if omitted"
},
"confirm": {
"type": "boolean",
"description": "true on a live key, after the user approves the preview"
},
"couponId": {
"type": "string",
"description": "coupon id to apply, from list_stripe_coupons"
},
"metadata": {
"type": "object",
"description": "key/value metadata stored on the code",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"expiresAt": {
"type": "string",
"description": "last day the code works, YYYY-MM-DD"
},
"customerId": {
"type": "string",
"description": "restrict the code to one customer (cus_…)"
},
"firstTimeOnly": {
"type": "boolean",
"description": "true = valid only on a customer's first purchase"
},
"minimumAmount": {
"type": "number",
"description": "MAJOR units"
},
"maxRedemptions": {
"type": "number",
"description": "maximum uses of this code"
},
"minimumAmountCurrency": {
"type": "string",
"description": "3-letter currency code for minimumAmount"
}
}
}| true = create the tag paused, so it does not fire |
| parameter | array | Tag Manager parameters, each { type, key, value }. type defaults to "template", which is Tag Manager's string type. |
| workspace | string | older name for gtmWorkspace (MCP only) |
| gtmWorkspace | string | Tag Manager workspace id or name. Defaults to the Default Workspace. |
| containerPath* | string | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers |
| firingTriggerId* | array | REQUIRED, at least one real trigger id from list_tag_manager_tags. A tag with no trigger is installed and never runs. |
| tagFiringOption | string | unlimited, oncePerEvent or oncePerLoad |
| blockingTriggerId | array | trigger ids that stop this tag firing when they match |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"containerPath",
"name",
"type",
"firingTriggerId"
],
"properties": {
"name": {
"type": "string",
"description": "what a human will look for in the Tag Manager UI"
},
"type": {
"type": "string",
"description": "the Tag Manager tag type string. Copy it off a tag of the same kind already in the container (list_tag_manager_tags) rather than guessing: Google publishes no machine-readable list of these."
},
"notes": {
"type": "string",
"description": "internal note stored on the tag; not shown on the website"
},
"paused": {
"type": "boolean",
"description": "true = create the tag paused, so it does not fire"
},
"parameter": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "Tag Manager parameters, each { type, key, value }. type defaults to \"template\", which is Tag Manager's string type."
},
"workspace": {
"type": "string",
"description": "older name for gtmWorkspace (MCP only)"
},
"gtmWorkspace": {
"type": "string",
"description": "Tag Manager workspace id or name. Defaults to the Default Workspace."
},
"containerPath": {
"type": "string",
"description": "the \"accounts/<id>/containers/<id>\" path from list_tag_manager_containers"
},
"firingTriggerId": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED, at least one real trigger id from list_tag_manager_tags. A tag with no trigger is installed and never runs."
},
"tagFiringOption": {
"type": "string",
"description": "unlimited, oncePerEvent or oncePerLoad"
},
"blockingTriggerId": {
"type": "array",
"items": {
"type": "string"
},
"description": "trigger ids that stop this tag firing when they match"
}
}
}| string |
| Tag Manager workspace id or name; defaults to the Default Workspace |
| containerPath* | string | container path accounts/<id>/containers/<id>, from list_tag_manager_containers |
| customEventFilter | array | REQUIRED for a customEvent trigger, or it would fire on every dataLayer event in the container |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"containerPath",
"name",
"type"
],
"properties": {
"name": {
"type": "string",
"description": "trigger name, as it appears in Tag Manager"
},
"type": {
"type": "string",
"description": "one of Tag Manager's own trigger types, e.g. pageview, domReady, windowLoaded, customEvent, formSubmission, linkClick, click, historyChange, scrollDepth, elementVisibility, timer. An unknown one is refused by name with the full list."
},
"notes": {
"type": "string",
"description": "internal note stored on the trigger"
},
"filter": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "Tag Manager Condition objects, each { type, parameter: [...] }"
},
"workspace": {
"type": "string",
"description": "older name for gtmWorkspace (MCP only)"
},
"gtmWorkspace": {
"type": "string",
"description": "Tag Manager workspace id or name; defaults to the Default Workspace"
},
"containerPath": {
"type": "string",
"description": "container path accounts/<id>/containers/<id>, from list_tag_manager_containers"
},
"customEventFilter": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "REQUIRED for a customEvent trigger, or it would fire on every dataLayer event in the container"
}
}
}| taxId |
| string |
| VAT, tax id or ABN depending on the country |
| company | string | legal name of the business that will advertise |
| confirm | boolean | REQUIRED true to create; call without it first to see exactly what would be created |
| currency | string | must match the Business Center currency; defaults to it |
| industry | number | TikTok tier-2 industry id; call without it to get the list for this Business Center |
| timezone | string | IANA time zone, e.g. America/New_York |
| licenseNo | string | business licence number, for places that require one |
| contactName | string |
| contactEmail | string |
| contactPhone | string |
| promotionLink | string | company website; required for an agency Business Center. Pass the brand site when you have it: Hermoso sends it only where TikTok takes it and the reply says when it was not sent |
| billingAddress | string | required for France, Brazil and Mexico |
| registeredArea | string | two-letter country of registration, e.g. US |
| qualificationId | string | reuse a qualification already verified in this Business Center |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers; omit when the login administers exactly one"
},
"name": {
"type": "string",
"description": "ad account name, up to 100 characters"
},
"share": {
"type": "boolean",
"description": "default true: tick the new account on this brand's TikTok Ads connection"
},
"taxId": {
"type": "string",
"description": "VAT, tax id or ABN depending on the country"
},
"company": {
"type": "string",
"description": "legal name of the business that will advertise"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create; call without it first to see exactly what would be created"
},
"currency": {
"type": "string",
"description": "must match the Business Center currency; defaults to it"
},
"industry": {
"type": "number",
"description": "TikTok tier-2 industry id; call without it to get the list for this Business Center"
},
"timezone": {
"type": "string",
"description": "IANA time zone, e.g. America/New_York"
},
"licenseNo": {
"type": "string",
"description": "business licence number, for places that require one"
},
"contactName": {
"type": "string"
},
"contactEmail": {
"type": "string"
},
"contactPhone": {
"type": "string"
},
"promotionLink": {
"type": "string",
"description": "company website; required for an agency Business Center. Pass the brand site when you have it: Hermoso sends it only where TikTok takes it and the reply says when it was not sent"
},
"billingAddress": {
"type": "string",
"description": "required for France, Brazil and Mexico"
},
"registeredArea": {
"type": "string",
"description": "two-letter country of registration, e.g. US"
},
"qualificationId": {
"type": "string",
"description": "reuse a qualification already verified in this Business Center"
}
}
}| adText | string | the ad copy people read |
| pageId | string | LEAD ADS — the Instant Form the ad opens, from list_tiktok_ads_lead_forms (a PUBLISHED one). Use it under a LEAD_GENERATION campaign whose ad group has promotionTargetType INSTANT_PAGE. Checked first, for free: a draft form, a form on another account, or an ad account that has not accepted TikTok’s Lead Ads terms is refused by name. The reply reads the form back off TikTok’s stored ad. |
| musicId | string | CAROUSEL ADS — the sound, from list_tiktok_ads_music. Required with adFormat CAROUSEL_ADS, which takes the slide images (1-35) as imageIds and no videoId; refused here for free otherwise. |
| videoId | string | the videoId returned by upload_tiktok_ads_creative (or a video already in the advertiser’s TikTok asset library) |
| adFormat | string | default SINGLE_VIDEO |
| imageIds | array | for a video ad, the ONE cover image: pass [coverImageId] from the same upload_tiktok_ads_creative call. For the image formats, the images themselves. |
| adgroupId* | string | the ad group whose targeting, budget and schedule this ad runs under |
| utmParams | object | URL parameters (UTMs) 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__, __AID__, __CID__, __PLACEMENT__ …). |
| identityId* | string | REQUIRED — from list_tiktok_ads_identities; the TikTok account the ad posts as |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| callToAction | string | TikTok’s CTA button value — default LEARN_MORE |
| identityType* | string | REQUIRED — the matching type reported next to that id by list_tiktok_ads_identities. There is NO default and guessing is refused: the plausible-looking CUSTOMIZED_USER is wrong for a plain TikTok account, which is TT_USER. |
| tiktokItemId | string | SPARK ADS — promote an EXISTING organic TikTok post instead of an uploaded video. From list_tiktok_ads_identity_posts (an account you control) or list_tiktok_ads_spark_posts (a creator post authorised by code). Mutually exclusive with videoId, and NOT supported with identityType CUSTOMIZED_USER; both are refused here for free. |
| landingPageUrl | string | where the ad sends people — not needed on an Instant-Form lead ad |
| identityAuthorizedBcId | string | REQUIRED when identityType is BC_AUTH_TT — the Business Center id reported next to that identity by list_tiktok_ads_identities |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adgroupId",
"name",
"identityId",
"identityType"
],
"properties": {
"name": {
"type": "string",
"description": "the ad name in TikTok Ads Manager — not the copy people see"
},
"adText": {
"type": "string",
"description": "the ad copy people read"
},
"pageId": {
"type": "string",
"description": "LEAD ADS — the Instant Form the ad opens, from list_tiktok_ads_lead_forms (a PUBLISHED one). Use it under a LEAD_GENERATION campaign whose ad group has promotionTargetType INSTANT_PAGE. Checked first, for free: a draft form, a form on another account, or an ad account that has not accepted TikTok’s Lead Ads terms is refused by name. The reply reads the form back off TikTok’s stored ad."
},
"musicId": {
"type": "string",
"description": "CAROUSEL ADS — the sound, from list_tiktok_ads_music. Required with adFormat CAROUSEL_ADS, which takes the slide images (1-35) as imageIds and no videoId; refused here for free otherwise."
},
"videoId": {
"type": "string",
"description": "the videoId returned by upload_tiktok_ads_creative (or a video already in the advertiser’s TikTok asset library)"
},
"adFormat": {
"type": "string",
"description": "default SINGLE_VIDEO"
},
"imageIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "for a video ad, the ONE cover image: pass [coverImageId] from the same upload_tiktok_ads_creative call. For the image formats, the images themselves."
},
"adgroupId": {
"type": "string",
"description": "the ad group whose targeting, budget and schedule this ad runs under"
},
"utmParams": {
"type": "object",
"description": "URL parameters (UTMs) 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__, __AID__, __CID__, __PLACEMENT__ …).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"identityId": {
"type": "string",
"description": "REQUIRED — from list_tiktok_ads_identities; the TikTok account the ad posts as"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"callToAction": {
"type": "string",
"description": "TikTok’s CTA button value — default LEARN_MORE"
},
"identityType": {
"type": "string",
"description": "REQUIRED — the matching type reported next to that id by list_tiktok_ads_identities. There is NO default and guessing is refused: the plausible-looking CUSTOMIZED_USER is wrong for a plain TikTok account, which is TT_USER."
},
"tiktokItemId": {
"type": "string",
"description": "SPARK ADS — promote an EXISTING organic TikTok post instead of an uploaded video. From list_tiktok_ads_identity_posts (an account you control) or list_tiktok_ads_spark_posts (a creator post authorised by code). Mutually exclusive with videoId, and NOT supported with identityType CUSTOMIZED_USER; both are refused here for free."
},
"landingPageUrl": {
"type": "string",
"description": "where the ad sends people — not needed on an Instant-Form lead ad"
},
"identityAuthorizedBcId": {
"type": "string",
"description": "REQUIRED when identityType is BC_AUTH_TT — the Business Center id reported next to that identity by list_tiktok_ads_identities"
}
}
}| number |
| ad-group budget in the advertiser’s own currency |
| pacing | string | default PACING_MODE_SMOOTH, which spends the budget evenly across the day. PACING_MODE_FAST spends as fast as delivery allows — a real money decision, so it is never chosen for you. |
| bidType | string | default BID_TYPE_NO_BID (let TikTok bid). A manual bid type needs bid. |
| genders | array | omit to reach everyone |
| pixelId | string | REQUIRED when optimizationGoal is CONVERT or VALUE, and NOT ACCEPTED with any other goal — that is TikTok’s own two-sided rule, and it is why a conversion campaign built without one dies at the ad group with "Please select a pixel." Get it from list_tiktok_ads_pixels, or create_tiktok_ads_pixel for a new one. |
| ageGroups | array | TikTok age band values — omit to reach every age |
| languages | array | language codes from search_tiktok_ads_targeting(kind:"language") |
| budgetMode | string | default BUDGET_MODE_DAY |
| campaignId* | string | the campaign this ad group belongs to |
| placements | array | default ["PLACEMENT_TIKTOK"] |
| locationIds* | array | REQUIRED in practice — ids from search_tiktok_ads_targeting(kind:"location"); TikTok refuses an ad group without them |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| billingEvent | string | what TikTok charges for — default CPC; must fit the optimization goal |
| scheduleType | string | default SCHEDULE_FROM_NOW |
| conversionBid | number | target cost per conversion for oCPM — required by TikTok when bidType is BID_TYPE_CUSTOM and billingEvent is OCPM |
| placementType | string | default PLACEMENT_TYPE_NORMAL |
| promotionType | string | what is being promoted — default WEBSITE |
| scheduleEndTime | string | only needed when the schedule type is a fixed window |
| optimizationGoal | string | what TikTok optimises delivery toward — default CLICK; it has to fit the campaign objective and TikTok’s refusal names the valid values |
| optimizationEvent | string | REQUIRED whenever pixelId is given (and when optimizationGoal is IN_APP_EVENT or VALUE) — the conversion TikTok optimises toward. Web events: ON_WEB_DETAIL, ON_WEB_SEARCH, ON_WEB_ADD_TO_WISHLIST, ON_WEB_CART, INITIATE_ORDER, ADD_BILLING, SHOPPING, CONSULT, DOWNLOAD_START, FORM, ON_WEB_REGISTER, ON_WEB_SUBSCRIBE. Pick the one that matches the goal: SHOPPING for a purchase, FORM for a lead, ON_WEB_REGISTER for a signup. The event must already EXIST on the pixel or TikTok refuses with "This pixel event type does not exist." |
| scheduleStartTime | string | "YYYY-MM-DD HH:MM:SS" in the advertiser’s timezone |
| customConversionId | string | optional narrower rule from list_tiktok_ads_custom_conversions — valid only alongside a pixel and only when optimizationGoal is CONVERT or IN_APP_EVENT |
| interestKeywordIds | array | ids from search_tiktok_ads_targeting(kind:"interest_keyword") — TikTok pairs this with interestCategoryIds under Interests |
| interestCategoryIds | array | ids from search_tiktok_ads_targeting(kind:"interest") |
| promotionTargetType | string | LEAD_GENERATION campaigns only: INSTANT_PAGE (a TikTok in-app form) or EXTERNAL_WEBSITE (your own landing page). INSTANT_PAGE defaults promotionType and optimizationGoal to LEAD_GENERATION and billingEvent to OCPM, which is TikTok’s documented Instant-Form ad group; then attach the form on the ad with create_tiktok_ads_ad(pageId). With EXTERNAL_WEBSITE + optimizationGoal CLICK, TikTok forbids pixelId and optimizationEvent. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name",
"locationIds"
],
"properties": {
"bid": {
"type": "number",
"description": "bid price in the advertiser’s own currency — required by the manual bid types"
},
"name": {
"type": "string",
"description": "ad group name in TikTok Ads Manager"
},
"budget": {
"type": "number",
"description": "ad-group budget in the advertiser’s own currency"
},
"pacing": {
"type": "string",
"description": "default PACING_MODE_SMOOTH, which spends the budget evenly across the day. PACING_MODE_FAST spends as fast as delivery allows — a real money decision, so it is never chosen for you."
},
"bidType": {
"type": "string",
"description": "default BID_TYPE_NO_BID (let TikTok bid). A manual bid type needs bid."
},
"genders": {
"type": "array",
"items": {
"type": "string"
},
"description": "omit to reach everyone"
},
"pixelId": {
"type": "string",
"description": "REQUIRED when optimizationGoal is CONVERT or VALUE, and NOT ACCEPTED with any other goal — that is TikTok’s own two-sided rule, and it is why a conversion campaign built without one dies at the ad group with \"Please select a pixel.\" Get it from list_tiktok_ads_pixels, or create_tiktok_ads_pixel for a new one."
},
"ageGroups": {
"type": "array",
"items": {
"type": "string"
},
"description": "TikTok age band values — omit to reach every age"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "language codes from search_tiktok_ads_targeting(kind:\"language\")"
},
"budgetMode": {
"type": "string",
"description": "default BUDGET_MODE_DAY"
},
"campaignId": {
"type": "string",
"description": "the campaign this ad group belongs to"
},
"placements": {
"type": "array",
"items": {
"type": "string"
},
"description": "default [\"PLACEMENT_TIKTOK\"]"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED in practice — ids from search_tiktok_ads_targeting(kind:\"location\"); TikTok refuses an ad group without them"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"billingEvent": {
"type": "string",
"description": "what TikTok charges for — default CPC; must fit the optimization goal"
},
"scheduleType": {
"type": "string",
"description": "default SCHEDULE_FROM_NOW"
},
"conversionBid": {
"type": "number",
"description": "target cost per conversion for oCPM — required by TikTok when bidType is BID_TYPE_CUSTOM and billingEvent is OCPM"
},
"placementType": {
"type": "string",
"description": "default PLACEMENT_TYPE_NORMAL"
},
"promotionType": {
"type": "string",
"description": "what is being promoted — default WEBSITE"
},
"scheduleEndTime": {
"type": "string",
"description": "only needed when the schedule type is a fixed window"
},
"optimizationGoal": {
"type": "string",
"description": "what TikTok optimises delivery toward — default CLICK; it has to fit the campaign objective and TikTok’s refusal names the valid values"
},
"optimizationEvent": {
"type": "string",
"description": "REQUIRED whenever pixelId is given (and when optimizationGoal is IN_APP_EVENT or VALUE) — the conversion TikTok optimises toward. Web events: ON_WEB_DETAIL, ON_WEB_SEARCH, ON_WEB_ADD_TO_WISHLIST, ON_WEB_CART, INITIATE_ORDER, ADD_BILLING, SHOPPING, CONSULT, DOWNLOAD_START, FORM, ON_WEB_REGISTER, ON_WEB_SUBSCRIBE. Pick the one that matches the goal: SHOPPING for a purchase, FORM for a lead, ON_WEB_REGISTER for a signup. The event must already EXIST on the pixel or TikTok refuses with \"This pixel event type does not exist.\""
},
"scheduleStartTime": {
"type": "string",
"description": "\"YYYY-MM-DD HH:MM:SS\" in the advertiser’s timezone"
},
"customConversionId": {
"type": "string",
"description": "optional narrower rule from list_tiktok_ads_custom_conversions — valid only alongside a pixel and only when optimizationGoal is CONVERT or IN_APP_EVENT"
},
"interestKeywordIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ids from search_tiktok_ads_targeting(kind:\"interest_keyword\") — TikTok pairs this with interestCategoryIds under Interests"
},
"interestCategoryIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ids from search_tiktok_ads_targeting(kind:\"interest\")"
},
"promotionTargetType": {
"type": "string",
"description": "LEAD_GENERATION campaigns only: INSTANT_PAGE (a TikTok in-app form) or EXTERNAL_WEBSITE (your own landing page). INSTANT_PAGE defaults promotionType and optimizationGoal to LEAD_GENERATION and billingEvent to OCPM, which is TikTok’s documented Instant-Form ad group; then attach the form on the ad with create_tiktok_ads_ad(pageId). With EXTERNAL_WEBSITE + optimizationGoal CLICK, TikTok forbids pixelId and optimizationEvent."
}
}
}| advertiserId |
| string |
| from list_tiktok_ads_accounts |
| audienceType* | string | source of the audience; decides what eventSourceIds refer to (pixel, app, ad group, post, shop or offline set) |
| excludeRules | array | same shape; anyone matching these is removed from the audience |
| identityType | string | TT_USER or BC_AUTH_TT; used with identityId for the organic and live-video engagement types |
| retentionInDays | number | how long the AUDIENCE itself lives, 1-365. A DIFFERENT field from each rule’s retentionDays lookback window — TikTok says itself it will rename one of them. |
| identityAuthorizedBcId | string | REQUIRED when identityType is BC_AUTH_TT |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"audienceType",
"rules"
],
"properties": {
"name": {
"type": "string",
"description": "≤128 characters"
},
"rules": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "inclusion rules, combined with OR. Each is {event, retentionDays, eventSourceIds?, parameterFilters?} — e.g. {event:\"COMPLETE PAYMENT\", retentionDays:30, eventSourceIds:[\"<pixelId>\"]}"
},
"identityId": {
"type": "string",
"description": "REQUIRED for ENGAGEMENT_ORGANIC_VIDEO and ENGAGEMENT_LIVE_VIDEO — from list_tiktok_ads_identities"
},
"autoRefresh": {
"type": "boolean",
"description": "default true at TikTok — keeps the audience updating with new matching people"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"audienceType": {
"enum": [
"ENGAGEMENT",
"ENGAGEMENT_ORGANIC_VIDEO",
"ENGAGEMENT_LIVE_VIDEO",
"APP",
"PIXEL",
"LEAD_GENERATION",
"BUSINESS_ACCOUNT",
"TIKTOK_SHOP",
"OFFLINE"
],
"type": "string",
"description": "source of the audience; decides what eventSourceIds refer to (pixel, app, ad group, post, shop or offline set)"
},
"excludeRules": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "same shape; anyone matching these is removed from the audience"
},
"identityType": {
"enum": [
"TT_USER",
"BC_AUTH_TT"
],
"type": "string",
"description": "TT_USER or BC_AUTH_TT; used with identityId for the organic and live-video engagement types"
},
"retentionInDays": {
"type": "number",
"description": "how long the AUDIENCE itself lives, 1-365. A DIFFERENT field from each rule’s retentionDays lookback window — TikTok says itself it will rename one of them."
},
"identityAuthorizedBcId": {
"type": "string",
"description": "REQUIRED when identityType is BC_AUTH_TT"
}
}
}| string |
| optional contact block: give all of it or none |
| contactEmail | string |
| contactPhone | string |
| registeredArea | string | two-letter country of registration, e.g. US |
| secondaryIndustry | string | tier-2 industry id; needed with the contact block on SELF_SERVICE |
| contactPhoneRegion | string | two-letter region of the phone number |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"type": {
"enum": [
"SELF_SERVICE",
"SELF_SERVICE_AGENCY"
],
"type": "string",
"description": "default SELF_SERVICE, the user's own business; SELF_SERVICE_AGENCY for an agency"
},
"bcName": {
"type": "string",
"description": "a name for the Business Center; duplicates are refused"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to create, only after the user has agreed to the terms in the first reply"
},
"currency": {
"type": "string",
"description": "three-letter currency, e.g. USD; ad accounts inside must use it"
},
"timezone": {
"type": "string",
"description": "IANA time zone, e.g. America/New_York; cannot be changed later"
},
"companyName": {
"type": "string",
"description": "the legal name of the user's business"
},
"contactName": {
"type": "string",
"description": "optional contact block: give all of it or none"
},
"contactEmail": {
"type": "string"
},
"contactPhone": {
"type": "string"
},
"registeredArea": {
"type": "string",
"description": "two-letter country of registration, e.g. US"
},
"secondaryIndustry": {
"type": "string",
"description": "tier-2 industry id; needed with the contact block on SELF_SERVICE"
},
"contactPhoneRegion": {
"type": "string",
"description": "two-letter region of the phone number"
}
}
}| budgetMode | string | default BUDGET_MODE_DAY — or BUDGET_MODE_TOTAL / BUDGET_MODE_DYNAMIC_DAILY_BUDGET / BUDGET_MODE_INFINITE. RF_REACH requires BUDGET_MODE_INFINITE. |
| advertiserId | string | from list_tiktok_ads_accounts — omit only when exactly one is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "the campaign name in TikTok Ads Manager"
},
"budget": {
"type": "number",
"description": "campaign budget in the advertiser’s own currency — not needed with BUDGET_MODE_INFINITE"
},
"objective": {
"type": "string",
"description": "default TRAFFIC. e.g. TRAFFIC, REACH, VIDEO_VIEWS, ENGAGEMENT, WEB_CONVERSIONS, LEAD_GENERATION, APP_INSTALL, PRODUCT_SALES, CONVERSIONS — or RF_REACH with budgetMode BUDGET_MODE_INFINITE. The server validates it and refuses an unavailable one BY NAME with TikTok’s reason."
},
"budgetMode": {
"type": "string",
"description": "default BUDGET_MODE_DAY — or BUDGET_MODE_TOTAL / BUDGET_MODE_DYNAMIC_DAILY_BUDGET / BUDGET_MODE_INFINITE. RF_REACH requires BUDGET_MODE_INFINITE."
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit only when exactly one is reachable"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"regionCode",
"currency"
],
"properties": {
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers"
},
"name": {
"type": "string",
"description": "≤128 characters"
},
"channel": {
"enum": [
"PARTNER",
"CLIENT"
],
"type": "string",
"description": "also immutable once set"
},
"currency": {
"type": "string",
"description": "IMMUTABLE. The currency for that region, e.g. USD"
},
"regionCode": {
"type": "string",
"description": "IMMUTABLE. The primary targeting region, e.g. US"
},
"catalogType": {
"enum": [
"ECOM",
"HOTEL",
"FLIGHT",
"DESTINATION",
"ENTERTAINMENT",
"AUTO_VEHICLE",
"AUTO_MODEL",
"MINI_SERIES",
"GENERIC"
],
"type": "string",
"description": "default ECOM — what a shop selling physical products wants"
}
}
}| NARROW (closest to the seed), BALANCED, or BROAD (furthest reach) |
| includeSource* | boolean | REQUIRED, no default — true keeps the seed’s own people inside the lookalike |
| sourceAudienceId* | string | the seed, from list_tiktok_ads_audiences — at least 100 people, and not itself a lookalike |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"sourceAudienceId",
"audienceSize",
"includeSource",
"placements",
"locationIds"
],
"properties": {
"name": {
"type": "string",
"description": "≤128 characters"
},
"mobileOs": {
"enum": [
"ALL",
"IOS",
"ANDROID"
],
"type": "string",
"description": "default ALL"
},
"placements": {
"type": "array",
"items": {
"type": "string"
},
"description": "one or more of \"TikTok\", \"TopBuzz & BuzzVideo\", \"Pangle\" — spelled exactly as TikTok publishes them"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "two-letter codes from TikTok’s LOOKALIKE list, e.g. US, GB, DE, JP"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"audienceSize": {
"enum": [
"NARROW",
"BALANCED",
"BROAD"
],
"type": "string",
"description": "NARROW (closest to the seed), BALANCED, or BROAD (furthest reach)"
},
"includeSource": {
"type": "boolean",
"description": "REQUIRED, no default — true keeps the seed’s own people inside the lookalike"
},
"sourceAudienceId": {
"type": "string",
"description": "the seed, from list_tiktok_ads_audiences — at least 100 people, and not itself a lookalike"
}
}
}| ageGroups | array | TikTok age band values; omit to reach all ages |
| frequency* | number | impressions allowed per person within each frequencySchedule window; same value as the estimate |
| languages | array | language codes from search_tiktok_ads_targeting(kind:"language"); omit for all |
| requestId | string | TikTok’s own idempotency key on this endpoint. Reuse the same value on a retry after a timeout and TikTok will not double-book. |
| campaignId* | string | an RF_REACH campaign (create_tiktok_ads_campaign with objective RF_REACH and budgetMode BUDGET_MODE_INFINITE) |
| locationIds* | array | one country or region only |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| deliveryMode | string | STANDARD | SCHEDULE | SEQUENCE |
| shareDisabled | boolean | true turns off sharing of the ads |
| purchasedReach* | number | IN THOUSANDS, from the estimate |
| brandSafetyType | string | NO_BRAND_SAFETY (default), EXPANDED_INVENTORY, STANDARD_INVENTORY or LIMITED_INVENTORY |
| commentDisabled | boolean | true turns off comments on the ads |
| rfPurchasedType* | string | FIXED_BUDGET | FIXED_SHOW | FIXED_REACH |
| scheduleEndTime* | string | "YYYY-MM-DD HH:59:59" UTC; at most 90 days after the start |
| cpvVideoDuration | string | SIX_SECONDS — required when optimizationGoal is VIDEO_VIEW |
| operatingSystems | array | ANDROID, IOS or PC; omit for all |
| optimizationGoal | string | REACH (default) | VIDEO_VIEW | CLICK | POST_ENGAGEMENT | INSTALL — must fit the campaign objective |
| frequencySchedule* | number | length of the frequency window in days; same value as the estimate |
| scheduleStartTime* | string | "YYYY-MM-DD HH:00:00" UTC for the target region |
| interestCategoryIds | array | interest ids from search_tiktok_ads_targeting(kind:"interest") |
| purchasedImpression* | number | IN THOUSANDS, from the estimate |
| videoDownloadDisabled | boolean | true stops viewers downloading the video |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name",
"locationIds",
"rfPurchasedType",
"budget",
"purchasedImpression",
"purchasedReach",
"scheduleStartTime",
"scheduleEndTime",
"frequency",
"frequencySchedule"
],
"properties": {
"name": {
"type": "string",
"description": "ad group name in TikTok Ads Manager"
},
"budget": {
"type": "number",
"description": "from the estimate"
},
"gender": {
"type": "string",
"description": "TikTok gender value; omit to reach everyone"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED to book. Without it nothing is created and you get the sentence to show the user."
},
"feedType": {
"type": "string",
"description": "STANDARD_FEED (large inventory) or TOP_FEED (limited inventory, higher CPM)"
},
"ageGroups": {
"type": "array",
"items": {
"type": "string"
},
"description": "TikTok age band values; omit to reach all ages"
},
"frequency": {
"type": "number",
"description": "impressions allowed per person within each frequencySchedule window; same value as the estimate"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "language codes from search_tiktok_ads_targeting(kind:\"language\"); omit for all"
},
"requestId": {
"type": "string",
"description": "TikTok’s own idempotency key on this endpoint. Reuse the same value on a retry after a timeout and TikTok will not double-book."
},
"campaignId": {
"type": "string",
"description": "an RF_REACH campaign (create_tiktok_ads_campaign with objective RF_REACH and budgetMode BUDGET_MODE_INFINITE)"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "one country or region only"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"deliveryMode": {
"type": "string",
"description": "STANDARD | SCHEDULE | SEQUENCE"
},
"shareDisabled": {
"type": "boolean",
"description": "true turns off sharing of the ads"
},
"purchasedReach": {
"type": "number",
"description": "IN THOUSANDS, from the estimate"
},
"brandSafetyType": {
"type": "string",
"description": "NO_BRAND_SAFETY (default), EXPANDED_INVENTORY, STANDARD_INVENTORY or LIMITED_INVENTORY"
},
"commentDisabled": {
"type": "boolean",
"description": "true turns off comments on the ads"
},
"rfPurchasedType": {
"type": "string",
"description": "FIXED_BUDGET | FIXED_SHOW | FIXED_REACH"
},
"scheduleEndTime": {
"type": "string",
"description": "\"YYYY-MM-DD HH:59:59\" UTC; at most 90 days after the start"
},
"cpvVideoDuration": {
"type": "string",
"description": "SIX_SECONDS — required when optimizationGoal is VIDEO_VIEW"
},
"operatingSystems": {
"type": "array",
"items": {
"type": "string"
},
"description": "ANDROID, IOS or PC; omit for all"
},
"optimizationGoal": {
"type": "string",
"description": "REACH (default) | VIDEO_VIEW | CLICK | POST_ENGAGEMENT | INSTALL — must fit the campaign objective"
},
"frequencySchedule": {
"type": "number",
"description": "length of the frequency window in days; same value as the estimate"
},
"scheduleStartTime": {
"type": "string",
"description": "\"YYYY-MM-DD HH:00:00\" UTC for the target region"
},
"interestCategoryIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "interest ids from search_tiktok_ads_targeting(kind:\"interest\")"
},
"purchasedImpression": {
"type": "number",
"description": "IN THOUSANDS, from the estimate"
},
"videoDownloadDisabled": {
"type": "boolean",
"description": "true stops viewers downloading the video"
}
}
}| array |
| what it does when the conditions match |
| conditions* | array | when it fires |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| applyObjects* | array | what the rule watches |
| notification* | object | { notificationType: TASK_FINISH | ANY_CHANGES | NOT_NOTIFICATION, emailSetting } — note these emails go to the developer address on the app, not to the user |
| ruleExecInfo* | object | { execTimeType: PER_HALF_HOUR | CUSTOM | HALF_HOUR_IN_SPECIFIC_TIME_PERIOD | SPECIFIC_TIME_ACCURATE_ONCE, execTime, timePeriodInfo } — PER_HALF_HOUR runs every thirty minutes forever |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"applyObjects",
"conditions",
"actions",
"notification",
"ruleExecInfo"
],
"properties": {
"name": {
"type": "string",
"description": "what this rule is called in TikTok Ads Manager — up to 512 characters"
},
"tzone": {
"type": "string",
"description": "timezone for the rule's time windows, sent to TikTok as given"
},
"actions": {
"type": "array",
"items": {
"type": "object",
"required": [
"subjectType"
],
"properties": {
"value": {
"type": "object",
"description": "{ value, limit } — limit is REQUIRED for INCREASE and DECREASE and is the ceiling (INCREASE) or floor (DECREASE) the rule may never cross",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"valueType": {
"enum": [
"EXACT",
"PERCENT"
],
"type": "string",
"description": "EXACT (an absolute amount) or PERCENT (a percentage change)"
},
"actionType": {
"enum": [
"INCREASE",
"DECREASE",
"ADJUST_TO"
],
"type": "string",
"description": "required for DAILY_BUDGET / LIFETIME_BUDGET / BID. ADJUST_TO is treated as spend-increasing because it is only a decrease for objects currently above the target, which the rule does not read until it fires."
},
"subjectType": {
"enum": [
"TURN_ON",
"TURN_OFF",
"MESSAGE",
"DAILY_BUDGET",
"LIFETIME_BUDGET",
"BID"
],
"type": "string",
"description": "TURN_OFF, MESSAGE and a DECREASE cannot start or raise spend. TURN_ON, INCREASE and ADJUST_TO can."
},
"frequencyInfo": {
"type": "object",
"description": "{ type: ONLY_ONCE | ONCE_IN_24_H | ONCE_IN_48_H | ONCE_IN_1_W | CUSTOM, customFrequencyType, time, count } — how often a budget/bid action may repeat",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"description": "what it does when the conditions match"
},
"conditions": {
"type": "array",
"items": {
"type": "object",
"required": [
"subjectType",
"values"
],
"properties": {
"values": {
"type": "array",
"items": {
"type": "string"
},
"description": "BETWEEN takes exactly two"
},
"matchType": {
"type": "string",
"description": "GT | LT | BETWEEN | MATCH — plus CONTAINS / NOT_CONTAINS / START_WITH / END_WITH / STRING_EQUAL, which TikTok allows ONLY when subjectType is NAME"
},
"rangeType": {
"type": "string",
"description": "TODAY | YESTERDAY | PAST_THREE_DAYS | PAST_FIVE_DAYS | PAST_SEVEN_DAYS | LIFETIME"
},
"subjectType": {
"type": "string",
"description": "COST, CPA, CPC, CPM, CTR, CVR, CONVERSION, IMPRESSION, CLICK, RESULT, COST_PER_RESULT, DAILY_BUDGET_SPENDING_RATE, ROAS_PURCHASE, DAYS_SINCE_CREATION, NAME … or NO_CONDITION, which TikTok itself warns about because it matches everything, every run"
},
"calculationType": {
"type": "string",
"description": "ALL_OBJECTS | OF_EACH_OBJECT — COST only"
}
}
},
"description": "when it fires"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"applyObjects": {
"type": "array",
"items": {
"type": "object",
"required": [
"dimension",
"preConditionType"
],
"properties": {
"dimension": {
"enum": [
"CAMPAIGN",
"ADGROUP",
"AD"
],
"type": "string",
"description": "level the rule watches: CAMPAIGN, ADGROUP or AD"
},
"dimensionIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "required when preConditionType is SELECTED"
},
"preConditionType": {
"type": "string",
"description": "SELECTED names a FIXED set and requires dimensionIds. Every other value (ALL_ACTIVE_CAMPAIGN, ALL_ACTIVE_AD_GROUP, ALL_ACTIVE_AD, ALL_ACTIVE_AD_GROUP_UNDER_SELECTED, ALL_ACTIVE_AD_UNDER_SELECTED and their ALL_INACTIVE_ twins) is RE-RESOLVED BY TIKTOK AT EVERY RUN, so objects created after today fall inside it."
}
}
},
"description": "what the rule watches"
},
"notification": {
"type": "object",
"description": "{ notificationType: TASK_FINISH | ANY_CHANGES | NOT_NOTIFICATION, emailSetting } — note these emails go to the developer address on the app, not to the user",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"ruleExecInfo": {
"type": "object",
"description": "{ execTimeType: PER_HALF_HOUR | CUSTOM | HALF_HOUR_IN_SPECIFIC_TIME_PERIOD | SPECIFIC_TIME_ACCURATE_ONCE, execTime, timePeriodInfo } — PER_HALF_HOUR runs every thirty minutes forever",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}| callToActions | array | up to 3 TikTok call-to-action enums, e.g. LEARN_MORE, SHOP_NOW |
| landingPageUrl | string | the destination URL |
| adConfiguration | object | TikTok ad_configuration for Spark or catalog ads (identity_type, identity_id, product_set_id, …) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId"
],
"properties": {
"name": {
"type": "string",
"description": "ad name; \"\" lets TikTok name it after the ad id"
},
"adTexts": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 5 ad texts; required unless every creative is a tiktok_item_id Spark post"
},
"adGroupId": {
"type": "string",
"description": "from create_tiktok_smart_ad_group"
},
"creatives": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "up to 50 TikTok creative_info objects: {ad_format: \"SINGLE_VIDEO\" | \"CAROUSEL_ADS\", video_info: {video_id}, image_info: [{web_uri}], tiktok_item_id, identity_type, identity_id}. Ids come from upload_tiktok_ads_creative and list_tiktok_ads_identities"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"playableUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 5 playableUrl values from upload_tiktok_ads_playable. App Promotion ads with a SINGLE_VIDEO creative only; TikTok allowlists Playable Ads per advertiser"
},
"callToActions": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 3 TikTok call-to-action enums, e.g. LEARN_MORE, SHOP_NOW"
},
"landingPageUrl": {
"type": "string",
"description": "the destination URL"
},
"adConfiguration": {
"type": "object",
"description": "TikTok ad_configuration for Spark or catalog ads (identity_type, identity_id, product_set_id, …)",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}| object |
| targeting_spec object — what it accepts varies with TikTok’s targeting_optimization_mode |
| budgetMode | string | required with budget when the campaign has CBO off |
| campaignId* | string | from create_tiktok_smart_campaign |
| deepBidType | string | required when optimizationGoal is VALUE |
| advertiserId | string | TikTok advertiser id from list_tiktok_ads_accounts; omit when only one is connected |
| billingEvent* | string | OCPM (optimized CPM) or CPC |
| scheduleType* | string | SCHEDULE_FROM_NOW (starts now) or SCHEDULE_START_END (needs scheduleEndTime) |
| promotionType* | string | where the ad promotes: APP_ANDROID / APP_IOS for app objectives and for WEB_CONVERSIONS with salesDestination APP; WEBSITE for WEB_CONVERSIONS on a site; MINI_APP / MINI_GAME / NATIVE_SERIES for APP_PROMOTION with MINIS; the four LEAD_GEN* values for LEAD_GENERATION |
| scheduleEndTime | string | YYYY-MM-DD HH:MM:SS end in the advertiser timezone; needed for SCHEDULE_START_END |
| optimizationGoal* | string | what the ad group optimizes for, e.g. CLICK, CONVERT or VALUE; must fit promotionType |
| optimizationEvent | string | the conversion event, required whenever pixelId is set (e.g. SHOPPING, ON_WEB_ORDER) |
| scheduleStartTime* | string | YYYY-MM-DD HH:MM:SS in the advertiser timezone |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"name",
"promotionType",
"optimizationGoal",
"bidType",
"billingEvent",
"targeting",
"scheduleType",
"scheduleStartTime"
],
"properties": {
"bid": {
"type": "number",
"description": "the Cost Cap target, required with bidType BID_TYPE_CUSTOM. Sent as bid_price for CLICK and conversion_bid_price for CONVERT, TRAFFIC_LANDING_PAGE_VIEW, INSTALL and IN_APP_EVENT; VALUE takes deepBidType and roasBid instead"
},
"name": {
"type": "string",
"description": "ad group name"
},
"budget": {
"type": "number",
"description": "ad group budget in the advertiser currency. Required when the campaign has budgetOptimizeOn false, ignored when CBO is on"
},
"bidType": {
"enum": [
"BID_TYPE_NO_BID",
"BID_TYPE_CUSTOM"
],
"type": "string",
"description": "BID_TYPE_NO_BID (automatic bidding) or BID_TYPE_CUSTOM (cost cap, needs bid)"
},
"pixelId": {
"type": "string",
"description": "required for WEB_CONVERSIONS / LEAD_GENERATION when optimizationGoal is CONVERT or VALUE"
},
"roasBid": {
"type": "number",
"description": "target ROAS (0.01-1000), required when deepBidType is VO_MIN_ROAS"
},
"targeting": {
"type": "object",
"description": "targeting_spec object — what it accepts varies with TikTok’s targeting_optimization_mode",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"budgetMode": {
"enum": [
"BUDGET_MODE_TOTAL",
"BUDGET_MODE_DYNAMIC_DAILY_BUDGET"
],
"type": "string",
"description": "required with budget when the campaign has CBO off"
},
"campaignId": {
"type": "string",
"description": "from create_tiktok_smart_campaign"
},
"deepBidType": {
"enum": [
"DEFAULT",
"AEO",
"VO_MIN_ROAS",
"VO_HIGHEST_VALUE"
],
"type": "string",
"description": "required when optimizationGoal is VALUE"
},
"advertiserId": {
"type": "string",
"description": "TikTok advertiser id from list_tiktok_ads_accounts; omit when only one is connected"
},
"billingEvent": {
"enum": [
"OCPM",
"CPC"
],
"type": "string",
"description": "OCPM (optimized CPM) or CPC"
},
"scheduleType": {
"enum": [
"SCHEDULE_FROM_NOW",
"SCHEDULE_START_END"
],
"type": "string",
"description": "SCHEDULE_FROM_NOW (starts now) or SCHEDULE_START_END (needs scheduleEndTime)"
},
"promotionType": {
"enum": [
"APP_ANDROID",
"APP_IOS",
"WEBSITE",
"MINI_APP",
"MINI_GAME",
"NATIVE_SERIES",
"LEAD_GENERATION",
"LEAD_GEN_CLICK_TO_TT_DIRECT_MESSAGE",
"LEAD_GEN_CLICK_TO_SOCIAL_MEDIA_APP_MESSAGE",
"LEAD_GEN_CLICK_TO_CALL"
],
"type": "string",
"description": "where the ad promotes: APP_ANDROID / APP_IOS for app objectives and for WEB_CONVERSIONS with salesDestination APP; WEBSITE for WEB_CONVERSIONS on a site; MINI_APP / MINI_GAME / NATIVE_SERIES for APP_PROMOTION with MINIS; the four LEAD_GEN* values for LEAD_GENERATION"
},
"scheduleEndTime": {
"type": "string",
"description": "YYYY-MM-DD HH:MM:SS end in the advertiser timezone; needed for SCHEDULE_START_END"
},
"optimizationGoal": {
"enum": [
"CLICK",
"INSTALL",
"IN_APP_EVENT",
"VALUE",
"CONVERT",
"TRAFFIC_LANDING_PAGE_VIEW",
"CONVERSATION",
"LEAD_GENERATION"
],
"type": "string",
"description": "what the ad group optimizes for, e.g. CLICK, CONVERT or VALUE; must fit promotionType"
},
"optimizationEvent": {
"type": "string",
"description": "the conversion event, required whenever pixelId is set (e.g. SHOPPING, ON_WEB_ORDER)"
},
"scheduleStartTime": {
"type": "string",
"description": "YYYY-MM-DD HH:MM:SS in the advertiser timezone"
}
}
}| required when catalogEnabled, and the valid set depends on the OBJECTIVE: WEB_CONVERSIONS takes ECOMMERCE / TRAVEL_ENTERTAINMENT / MINI_SERIES / GENERIC, LEAD_GENERATION takes GENERIC only, APP_PROMOTION takes ONLINE_TO_OFFLINE only. MINI_SERIES is allowlist-only at TikTok |
| advertiserId | string | which advertiser; omit when the profile has one |
| catalogEnabled | boolean | make it a catalog campaign |
| appPromotionType | string | REQUIRED when the objective is APP_PROMOTION. MINIS promotes a Series or mini game |
| budgetOptimizeOn | boolean | campaign budget optimisation (CBO). TikTok is retiring non-CBO App Install VBO campaigns via the API at the end of September 2026, so leave this true for APP_INSTALL |
| salesDestination | string | REQUIRED for WEB_CONVERSIONS — where the sales happen. APP additionally needs a catalog |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"objective"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"budget": {
"type": "number",
"description": "campaign budget in the advertiser currency"
},
"objective": {
"enum": [
"APP_PROMOTION",
"WEB_CONVERSIONS",
"LEAD_GENERATION"
],
"type": "string",
"description": "Smart+ supports these three ONLY"
},
"budgetMode": {
"enum": [
"BUDGET_MODE_DYNAMIC_DAILY_BUDGET",
"BUDGET_MODE_TOTAL",
"BUDGET_MODE_INFINITE",
"BUDGET_MODE_DAY"
],
"type": "string",
"description": "the valid set depends on budgetOptimizeOn: DYNAMIC_DAILY_BUDGET or TOTAL with CBO on (the default), INFINITE / DAY / TOTAL with CBO off. Everything except INFINITE also needs budget"
},
"catalogType": {
"enum": [
"ECOMMERCE",
"TRAVEL_ENTERTAINMENT",
"MINI_SERIES",
"GENERIC",
"ONLINE_TO_OFFLINE"
],
"type": "string",
"description": "required when catalogEnabled, and the valid set depends on the OBJECTIVE: WEB_CONVERSIONS takes ECOMMERCE / TRAVEL_ENTERTAINMENT / MINI_SERIES / GENERIC, LEAD_GENERATION takes GENERIC only, APP_PROMOTION takes ONLINE_TO_OFFLINE only. MINI_SERIES is allowlist-only at TikTok"
},
"advertiserId": {
"type": "string",
"description": "which advertiser; omit when the profile has one"
},
"catalogEnabled": {
"type": "boolean",
"description": "make it a catalog campaign"
},
"appPromotionType": {
"enum": [
"APP_INSTALL",
"APP_RETARGETING",
"MINIS"
],
"type": "string",
"description": "REQUIRED when the objective is APP_PROMOTION. MINIS promotes a Series or mini game"
},
"budgetOptimizeOn": {
"type": "boolean",
"description": "campaign budget optimisation (CBO). TikTok is retiring non-CBO App Install VBO campaigns via the API at the end of September 2026, so leave this true for APP_INSTALL"
},
"salesDestination": {
"enum": [
"WEBSITE",
"APP"
],
"type": "string",
"description": "REQUIRED for WEB_CONVERSIONS — where the sales happen. APP additionally needs a catalog"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"brandName",
"brandIndustryId",
"brandWebsite",
"logoUrl"
],
"properties": {
"logoUrl": {
"type": "string",
"description": "PERMANENT: JPG/PNG/GIF, 1:1, ≤5MB, public URL"
},
"brandName": {
"type": "string",
"description": "PERMANENT: ≤60 characters"
},
"brandWebsite": {
"type": "string",
"description": "must start with https://"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
},
"brandIndustryId": {
"type": "string",
"description": "PERMANENT: TikTok’s Brand Profile industry id"
},
"tiktokAccountUrl": {
"type": "string",
"description": "https://www.tiktok.com/@handle"
}
}
}| string |
| default CAMPAIGN |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
| advertiserIds | array | max 50: these ad accounts get AUTOMATIC Spark Ads rights on every linked video |
| brandProfileId | string | required for BRAND_LINK; optional alternative to brandName on CAMPAIGN |
| sendNotification | boolean | default false: true puts an invitation in each creator’s TikTok inbox |
| campaignDescription | string | ≤1000 characters: positioning, audience, core features |
| businessAccountHandle | string | allowlist-only at TikTok, and non-EU only: lets creators message the brand from the invite link |
| sparkAdsAuthorizationDays | number | 0-365; REQUIRED when advertiserIds is given |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"anchorId": {
"type": "string",
"description": "from manage_tiktok_tto_anchor"
},
"brandName": {
"type": "string",
"description": "≤60 characters, when there is no Brand Profile"
},
"handleNames": {
"type": "array",
"items": {
"type": "string"
},
"description": "required for CAMPAIGN: TikTok usernames without the @, max 100"
},
"campaignName": {
"type": "string",
"description": "required for CAMPAIGN; shown to creators, ≤120 characters: TikTok suggests [product name - product description]"
},
"campaignType": {
"enum": [
"CAMPAIGN",
"BRAND_LINK"
],
"type": "string",
"description": "default CAMPAIGN"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
},
"advertiserIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "max 50: these ad accounts get AUTOMATIC Spark Ads rights on every linked video"
},
"brandProfileId": {
"type": "string",
"description": "required for BRAND_LINK; optional alternative to brandName on CAMPAIGN"
},
"sendNotification": {
"type": "boolean",
"description": "default false: true puts an invitation in each creator’s TikTok inbox"
},
"campaignDescription": {
"type": "string",
"description": "≤1000 characters: positioning, audience, core features"
},
"businessAccountHandle": {
"type": "string",
"description": "allowlist-only at TikTok, and non-EU only: lets creators message the brand from the invite link"
},
"sparkAdsAuthorizationDays": {
"type": "number",
"description": "0-365; REQUIRED when advertiserIds is given"
}
}
}| twitterIds |
| array |
| X user ids to add; hashed on Hermoso's server before X receives them |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| description | string | optional description of the audience |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "unique per account"
},
"users": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId}"
},
"emails": {
"type": "array",
"items": {
"type": "string"
},
"description": "plain addresses or 64-hex SHA-256 hashes"
},
"phones": {
"type": "array",
"items": {
"type": "string"
},
"description": "E.164, e.g. +14155550100, or hashes"
},
"handles": {
"type": "array",
"items": {
"type": "string"
},
"description": "X handles, with or without @"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"deviceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "IDFA / AdID / Android id"
},
"expiresAt": {
"type": "string",
"description": "ISO 8601; X defaults to 13 months"
},
"twitterIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "X user ids to add; hashed on Hermoso's server before X receives them"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"description": {
"type": "string",
"description": "optional description of the audience"
}
}
}| fundingInstrumentId | string | omit to be shown the account’s usable funding instruments |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "campaign name"
},
"endTime": {
"type": "string",
"description": "ISO 8601 end time; X deprecates campaign schedules, so set the schedule on the line item"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"startTime": {
"type": "string",
"description": "ISO 8601"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"dailyBudget": {
"type": "number",
"description": "in the ad account’s currency"
},
"totalBudget": {
"type": "number",
"description": "lifetime budget in ad account currency"
},
"fundingInstrumentId": {
"type": "string",
"description": "omit to be shown the account’s usable funding instruments"
}
}
}| X lead form id from list_x_ads_lead_forms, for a lead-form card |
| websiteUrl | string | destination URL for a website card |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| buttonLabel | string | button text shown on a lead-form or app card |
| countryCode | string | 2-letter store country for an app card, e.g. US |
| iosDeepLink | string | iOS deep link for an app card |
| googlePlayAppId | string | Google Play package id, for an app card |
| googlePlayDeepLink | string | Android deep link for an app card |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term tag appended to websiteUrl; a tag already in the URL is kept"
},
"medium": {
"type": "string",
"description": "utm_medium tag appended to websiteUrl; a tag already in the URL is kept"
},
"source": {
"type": "string",
"description": "utm_source tag appended to websiteUrl; a tag already in the URL is kept"
},
"content": {
"type": "string",
"description": "utm_content tag appended to websiteUrl; a tag already in the URL is kept"
},
"campaign": {
"type": "string",
"description": "utm_campaign tag appended to websiteUrl; a tag already in the URL is kept"
}
},
"description": "UTM tags object: source, medium, campaign, content, term; appended to websiteUrl"
},
"name": {
"type": "string",
"description": "internal name for the card"
},
"title": {
"type": "string",
"description": "headline, for website and lead-form cards"
},
"iosAppId": {
"type": "string",
"description": "iOS App Store app id, for an app card"
},
"mediaKey": {
"type": "string",
"description": "one media key from upload_x_ads_media (single image or video)"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"mediaKeys": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-6 media keys from upload_x_ads_media, for a swipeable card"
},
"leadFormId": {
"type": "string",
"description": "X lead form id from list_x_ads_lead_forms, for a lead-form card"
},
"websiteUrl": {
"type": "string",
"description": "destination URL for a website card"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"buttonLabel": {
"enum": [
"BOOK",
"CONNECT",
"INSTALL",
"OPEN",
"ORDER",
"PLAY",
"SHOP"
],
"type": "string",
"description": "button text shown on a lead-form or app card"
},
"countryCode": {
"type": "string",
"description": "2-letter store country for an app card, e.g. US"
},
"iosDeepLink": {
"type": "string",
"description": "iOS deep link for an app card"
},
"googlePlayAppId": {
"type": "string",
"description": "Google Play package id, for an app card"
},
"googlePlayDeepLink": {
"type": "string",
"description": "Android deep link for an app card"
}
}
}| description |
| string |
| intro card text |
| thankYouUrl | string | https link on the thank-you button |
| thankYouButton | string | label of the thank-you page button |
| privacyPolicyUrl* | string | https |
| thankYouHeadline | string | headline on the thank-you page shown after submit |
| conversionEventId | string | Event ID from X Events Manager — required when the ad account has no X Pixel |
| thankYouDescription | string | text on the thank-you page shown after submit |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name",
"purpose",
"fields",
"privacyPolicyUrl",
"companyName"
],
"properties": {
"name": {
"type": "string",
"description": "internal name of the lead form"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"enum": [
"EMAIL",
"PHONE_NUMBER",
"STREET_ADDRESS",
"CITY",
"STATE",
"COUNTRY",
"ZIP_CODE",
"FULL_NAME",
"FIRST_NAME",
"LAST_NAME",
"DATE_OF_BIRTH",
"COMPANY_NAME",
"JOB_TITLE",
"WORK_EMAIL",
"WORK_PHONE_NUMBER",
"GENDER",
"HANDLE"
],
"type": "string",
"description": "lead field the form collects, from X's fixed list of lead field keys"
},
"optional": {
"type": "boolean",
"description": "true = the person may skip this field; at least one field must not be optional"
}
}
},
"description": "at least one must not be optional"
},
"purpose": {
"type": "string",
"description": "what the leads are used for (shown to the person)"
},
"headline": {
"type": "string",
"description": "intro card headline"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"companyName": {
"type": "string",
"description": "company name shown on the form"
},
"description": {
"type": "string",
"description": "intro card text"
},
"thankYouUrl": {
"type": "string",
"description": "https link on the thank-you button"
},
"thankYouButton": {
"type": "string",
"description": "label of the thank-you page button"
},
"privacyPolicyUrl": {
"type": "string",
"description": "https"
},
"thankYouHeadline": {
"type": "string",
"description": "headline on the thank-you page shown after submit"
},
"conversionEventId": {
"type": "string",
"description": "Event ID from X Events Manager — required when the ad account has no X Pixel"
},
"thankYouDescription": {
"type": "string",
"description": "text on the thank-you page shown after submit"
}
}
}| startTime | string | ISO 8601; defaults to now |
| campaignId* | string | from create_x_ads_campaign or list_x_ads_campaigns |
| placements | array | default ALL_ON_TWITTER |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| bidStrategy | string | AUTO lets X set the bid; MAX and TARGET need bidAmount |
| productType | string | default PROMOTED_TWEETS — PROMOTED_ACCOUNT when objective is FOLLOWERS, and that line item serves the ACCOUNT attached with create_x_ads_promoted_account |
| totalBudget | number | lifetime budget for this line item in ad account currency |
| standardDelivery | boolean | false = accelerated, only with line item budgets; X refuses it under a campaign budget |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"objective"
],
"properties": {
"goal": {
"enum": [
"APP_CLICKS",
"APP_INSTALLS",
"APP_PURCHASES",
"ENGAGEMENT",
"FOLLOWERS",
"LINK_CLICKS",
"MAX_REACH",
"PREROLL",
"PREROLL_STARTS",
"REACH_WITH_ENGAGEMENT",
"SITE_VISITS",
"SOCIAL_ENGAGEMENT",
"VIDEO_VIEW",
"VIEW_15S",
"VIEW_3S_100PCT",
"VIEW_6S",
"WEBSITE_CONVERSIONS",
"WEBSITE_CONVERSIONS_V2"
],
"type": "string",
"description": "optimization goal, e.g. LINK_CLICKS or WEBSITE_CONVERSIONS; these are goals, not objectives"
},
"name": {
"type": "string",
"description": "line item (ad group) name"
},
"endTime": {
"type": "string",
"description": "ISO 8601 end time for this line item"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"bidAmount": {
"type": "number",
"description": "required for MAX/TARGET; in the ad account's currency"
},
"objective": {
"enum": [
"APP_ENGAGEMENTS",
"APP_INSTALLS",
"ENGAGEMENTS",
"FOLLOWERS",
"PREROLL_VIEWS",
"REACH",
"VIDEO_VIEWS",
"WEBSITE_CLICKS"
],
"type": "string",
"description": "one per campaign (X refuses a second objective). For an X LEAD AD use WEBSITE_CLICKS with a lead-form card — X retired LEAD_GENERATION"
},
"startTime": {
"type": "string",
"description": "ISO 8601; defaults to now"
},
"campaignId": {
"type": "string",
"description": "from create_x_ads_campaign or list_x_ads_campaigns"
},
"placements": {
"type": "array",
"items": {
"enum": [
"ALL_ON_TWITTER",
"PUBLISHER_NETWORK",
"TAP_BANNER",
"TAP_FULL",
"TAP_FULL_LANDSCAPE",
"TAP_MRECT",
"TAP_NATIVE",
"TWITTER_MEDIA_VIEWER",
"TWITTER_PROFILE",
"TWITTER_REPLIES",
"TWITTER_SEARCH",
"TWITTER_TIMELINE"
],
"type": "string"
},
"description": "default ALL_ON_TWITTER"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"bidStrategy": {
"enum": [
"AUTO",
"GUARANTEED",
"MAX",
"TARGET"
],
"type": "string",
"description": "AUTO lets X set the bid; MAX and TARGET need bidAmount"
},
"productType": {
"enum": [
"MEDIA",
"PROMOTED_ACCOUNT",
"PROMOTED_TWEETS"
],
"type": "string",
"description": "default PROMOTED_TWEETS — PROMOTED_ACCOUNT when objective is FOLLOWERS, and that line item serves the ACCOUNT attached with create_x_ads_promoted_account"
},
"totalBudget": {
"type": "number",
"description": "lifetime budget for this line item in ad account currency"
},
"standardDelivery": {
"type": "boolean",
"description": "false = accelerated, only with line item budgets; X refuses it under a campaign budget"
}
}
}| lineItemId | string | attach to this line item right away |
| videoTitle | string | title of a video post |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| videoCtaUrl | string | URL the video call-to-action links to |
| promotedOnly | boolean | default true |
| confirmPublic | boolean | true is required with promotedOnly:false, to publish a public post |
| videoDescription | string | description of a video post |
| conversationSettings | string | who may reply to the post: EVERYONE, FOLLOWING or MENTIONED_USERS |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term tag appended to websiteUrl; a tag already in the URL is kept"
},
"medium": {
"type": "string",
"description": "utm_medium tag appended to websiteUrl; a tag already in the URL is kept"
},
"source": {
"type": "string",
"description": "utm_source tag appended to websiteUrl; a tag already in the URL is kept"
},
"content": {
"type": "string",
"description": "utm_content tag appended to websiteUrl; a tag already in the URL is kept"
},
"campaign": {
"type": "string",
"description": "utm_campaign tag appended to websiteUrl; a tag already in the URL is kept"
}
},
"description": "UTM tags object: source, medium, campaign, content, term; appended to the destination URL"
},
"name": {
"type": "string",
"description": "internal name in ads.x.com"
},
"text": {
"type": "string",
"description": "post text"
},
"cardUri": {
"type": "string",
"description": "card_uri from create_x_ads_card; use instead of mediaKeys"
},
"asUserId": {
"type": "string",
"description": "promotable user id to post as; omit for the ad account's own user"
},
"videoCta": {
"enum": [
"VISIT_SITE",
"WATCH_NOW"
],
"type": "string",
"description": "call-to-action button on a video post: VISIT_SITE or WATCH_NOW"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"mediaKeys": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 4 media keys from upload_x_ads_media"
},
"lineItemId": {
"type": "string",
"description": "attach to this line item right away"
},
"videoTitle": {
"type": "string",
"description": "title of a video post"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"videoCtaUrl": {
"type": "string",
"description": "URL the video call-to-action links to"
},
"promotedOnly": {
"type": "boolean",
"description": "default true"
},
"confirmPublic": {
"type": "boolean",
"description": "true is required with promotedOnly:false, to publish a public post"
},
"videoDescription": {
"type": "string",
"description": "description of a video post"
},
"conversationSettings": {
"enum": [
"EVERYONE",
"FOLLOWING",
"MENTIONED_USERS"
],
"type": "string",
"description": "who may reply to the post: EVERYONE, FOLLOWING or MENTIONED_USERS"
}
}
}| 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "REQUIRED."
},
"level": {
"enum": [
"campaign",
"adgroup",
"keyword",
"negative_keyword",
"ad",
"creative",
"asset",
"location_group",
"budget_order"
],
"type": "string",
"description": "REQUIRED."
},
"confirm": {
"type": "boolean",
"description": "REQUIRED. Nothing is deleted without it."
},
"confirmName": {
"type": "string",
"description": "Optional — echo the object’s exact name to prove you aimed at the right one."
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"brand"
],
"properties": {
"brand": {
"type": "string",
"description": "profile id or exact name from list_brands"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — this destroys the whole profile and cannot be undone"
},
"confirmName": {
"type": "string",
"description": "the workspace's EXACT name, required when it is not empty — copy it from the inventory this tool returned, after the user has agreed to it"
},
"confirmConnectors": {
"type": "number",
"description": "the number of connected accounts the inventory reported, required when there is at least one — the user must specifically agree to losing them, because reconnecting each needs a browser and no agent can do it"
}
}
}| keywordId | string | keyword criterion id (level:"keyword" — pass adGroupId too) |
| campaignId | string | campaign id (level:"campaign") |
| customerId | string | 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| confirmName | string | the object’s EXACT name, required when it has children / is live / has spent |
| resourceName | string | full resource name — REQUIRED for campaignAsset / adGroupAsset, accepted for any level |
| 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 |
| conversionActionId | string | conversion action id (level:"conversionAction") |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad id (level:\"ad\" — pass adGroupId too)"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad",
"keyword",
"campaignAsset",
"adGroupAsset",
"conversionAction"
],
"type": "string",
"description": "what to remove — default campaign"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — REMOVED is permanent"
},
"adGroupId": {
"type": "string",
"description": "ad group id (level:\"adGroup\"; REQUIRED as the parent for \"ad\" and \"keyword\")"
},
"keywordId": {
"type": "string",
"description": "keyword criterion id (level:\"keyword\" — pass adGroupId too)"
},
"campaignId": {
"type": "string",
"description": "campaign id (level:\"campaign\")"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"confirmName": {
"type": "string",
"description": "the object’s EXACT name, required when it has children / is live / has spent"
},
"resourceName": {
"type": "string",
"description": "full resource name — REQUIRED for campaignAsset / adGroupAsset, accepted for any level"
},
"confirmChildren": {
"type": "number",
"description": "the exact number of children reported by the unconfirmed call, required when it has any"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"conversionActionId": {
"type": "string",
"description": "conversion action id (level:\"conversionAction\")"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"offerId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually delete; without it nothing is deleted and the real product is reported back"
},
"offerId": {
"type": "string",
"description": "your offer id of the product to delete"
},
"feedLabel": {
"type": "string",
"description": "market label of the product, e.g. US; taken from the feed when it declares one"
},
"dataSource": {
"type": "string",
"description": "the feed it lives in, from list_merchant_data_sources"
},
"contentLanguage": {
"type": "string",
"description": "two-letter language code of the product; taken from the feed when it declares one"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId",
"audienceId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true — the deletion is permanent"
},
"audienceId": {
"type": "string",
"description": "the custom audience id (from list_meta_audiences)"
},
"adAccountId": {
"type": "string",
"description": "ad account id (act_… or digits)"
},
"confirmName": {
"type": "string",
"description": "the audience’s EXACT name, required when it holds people or has lookalikes"
},
"confirmChildren": {
"type": "number",
"description": "the exact number of derived lookalikes reported by the unconfirmed call, required when it has any"
}
}
}| string |
| ad group id (level:"adGroup"; REQUIRED as the parent for level:"ad" and level:"keyword") |
| keywordId | string | keyword id (level:"keyword" — pass adGroupId too) |
| campaignId | string | campaign id (level:"campaign"; also the parent for level:"adGroup" if you know it) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad id (level:\"ad\" — pass adGroupId too)"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad",
"keyword"
],
"type": "string",
"description": "what to delete — default campaign"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — the delete is permanent"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "ad group id (level:\"adGroup\"; REQUIRED as the parent for level:\"ad\" and level:\"keyword\")"
},
"keywordId": {
"type": "string",
"description": "keyword id (level:\"keyword\" — pass adGroupId too)"
},
"campaignId": {
"type": "string",
"description": "campaign id (level:\"campaign\"; also the parent for level:\"adGroup\" if you know it)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"confirmName": {
"type": "string",
"description": "the object’s EXACT name, required when it has children / is Active / has spent"
},
"confirmChildren": {
"type": "number",
"description": "the exact number of children reported by the unconfirmed call, required when it has any"
}
}
}| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad id to archive; use with level ad"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "what to archive — default campaign"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — archiving cannot be undone"
},
"adGroupId": {
"type": "string",
"description": "ad group id to archive; use with level adGroup"
},
"campaignId": {
"type": "string",
"description": "campaign id to archive; use with level campaign"
},
"confirmName": {
"type": "string",
"description": "the object’s EXACT name, required when it has children or is live"
},
"confirmChildren": {
"type": "number",
"description": "the exact number of children reported by the unconfirmed call, required when it has any"
}
}
}| adAccountId |
| string |
| Pinterest ad account id — omit to use the single shared account |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad id to archive; used with level:\"ad\""
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "what to archive — default campaign"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — archiving is not self-service reversible"
},
"adGroupId": {
"type": "string",
"description": "ad group id to archive; used with level:\"adGroup\""
},
"campaignId": {
"type": "string",
"description": "campaign id to archive; used with level:\"campaign\""
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"confirmName": {
"type": "string",
"description": "the object’s EXACT name, required when it has children or is live"
},
"confirmChildren": {
"type": "number",
"description": "the exact number of children reported by the unconfirmed call, required when it has any"
}
}
}| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"kind",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "id of the campaign, ad group or ad to remove"
},
"kind": {
"enum": [
"campaign",
"ad_group",
"ad"
],
"type": "string",
"description": "campaign, ad_group or ad: which type the id refers to"
},
"status": {
"enum": [
"DELETED",
"ARCHIVED"
],
"type": "string",
"description": "DELETED = permanent (default); ARCHIVED = out of service but reversible to PAUSED"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"confirmName": {
"type": "string",
"description": "the object’s EXACT name, required when it has children or is live"
},
"confirmChildren": {
"type": "number",
"description": "the exact number of children reported by the unconfirmed call, required when it has any"
}
}
}| string |
| what the comment is — default REPLY, since your own comments are replies |
| confirmText | string | REQUIRED with confirm — the comment’s exact text, read back from TikTok |
| advertiserId | string | from list_tiktok_ads_accounts |
| identityType* | string | from the same comment row |
| tiktokItemId* | string | from the same comment row |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"commentId",
"adId",
"tiktokItemId",
"identityId",
"identityType"
],
"properties": {
"adId": {
"type": "string",
"description": "from the same comment row"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — without it nothing is deleted and you get the sentence describing what would be"
},
"commentId": {
"type": "string",
"description": "from list_tiktok_ads_comments — one whose canDelete is true"
},
"identityId": {
"type": "string",
"description": "from the same comment row"
},
"commentType": {
"enum": [
"COMMENT",
"REPLY"
],
"type": "string",
"description": "what the comment is — default REPLY, since your own comments are replies"
},
"confirmText": {
"type": "string",
"description": "REQUIRED with confirm — the comment’s exact text, read back from TikTok"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"identityType": {
"enum": [
"TT_USER",
"CUSTOMIZED_USER"
],
"type": "string",
"description": "from the same comment row"
},
"tiktokItemId": {
"type": "string",
"description": "from the same comment row"
}
}
}| accountId |
| string |
| older name for adAccountId (MCP only) |
| lineItemId | string | REQUIRED for type "targeting_criterion" — X cannot look one up without its line item |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| confirmName | string | the target's exact name — required once it has children, is live, or has spent |
| confirmChildren | number | the real number of children, from the unconfirmed call |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"type",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "campaign/line-item id, or the id from list_x_ads_promoted_tweets / list_x_ads_targeting"
},
"type": {
"enum": [
"campaign",
"line_item",
"promoted_tweet",
"targeting_criterion",
"promoted_account",
"lead_form",
"card",
"media",
"custom_audience"
],
"type": "string",
"description": "kind of object the id names; for media, id is the media_key"
},
"confirm": {
"type": "boolean",
"description": "omit on the first call to see the blast radius"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"lineItemId": {
"type": "string",
"description": "REQUIRED for type \"targeting_criterion\" — X cannot look one up without its line item"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"confirmName": {
"type": "string",
"description": "the target's exact name — required once it has children, is live, or has spent"
},
"confirmChildren": {
"type": "number",
"description": "the real number of children, from the unconfirmed call"
}
}
}| audience age band: 18-24, 25-34, 35-44, 45-54 or 55+ |
| maxAvgViews | number | maximum average views per post |
| minAvgViews | number | minimum average views per post |
| countryCodes* | array | 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) |
| maxFollowers | number | maximum follower count |
| minFollowers | number | minimum follower count |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
| keywordSearch | string | fuzzy match, ≤100 characters |
| maxMedianViews | number | maximum median views per post |
| minMedianViews | number | minimum median views per post |
| stateProvinces | array | US only, and only when countryCodes is exactly ["US"] |
| contentLabelIds | array | what the creator posts about: from list_tiktok_creator_labels(labelType:"SEARCH") |
| maxCreatorPrice | number | maximum creator starting price in USD |
| minCreatorPrice | number | the creator’s starting price, USD |
| industryLabelIds | array | commercial categories they have worked in: same source |
| maxEngagementRate | number | maximum engagement rate as a fraction, 0-1 (0.05 = 5%) |
| minEngagementRate | number | 0-1, NOT a percentage |
| followerGenderRatio | string | audience gender split bucket, e.g. FEMALE_60 or MALE_50 |
| followerCountryCodes | array | where their AUDIENCE is, which is often not where they are |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"countryCodes"
],
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"pageSize": {
"type": "number",
"description": "1-200, default 24"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "language codes the creator posts in; optional"
},
"sortField": {
"enum": [
"RELEVANCE",
"FOLLOWERS",
"MEDIAN_VIEWS",
"ENGAGEMENT_RATE"
],
"type": "string",
"description": "RELEVANCE, FOLLOWERS, MEDIAN_VIEWS or ENGAGEMENT_RATE"
},
"sortOrder": {
"enum": [
"ASC",
"DESC"
],
"type": "string",
"description": "ASC or DESC"
},
"followerAge": {
"enum": [
"18-24",
"25-34",
"35-44",
"45-54",
"55+"
],
"type": "string",
"description": "audience age band: 18-24, 25-34, 35-44, 45-54 or 55+"
},
"maxAvgViews": {
"type": "number",
"description": "maximum average views per post"
},
"minAvgViews": {
"type": "number",
"description": "minimum average views per post"
},
"countryCodes": {
"type": "array",
"items": {
"type": "string"
},
"description": "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)"
},
"maxFollowers": {
"type": "number",
"description": "maximum follower count"
},
"minFollowers": {
"type": "number",
"description": "minimum follower count"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
},
"keywordSearch": {
"type": "string",
"description": "fuzzy match, ≤100 characters"
},
"maxMedianViews": {
"type": "number",
"description": "maximum median views per post"
},
"minMedianViews": {
"type": "number",
"description": "minimum median views per post"
},
"stateProvinces": {
"type": "array",
"items": {
"type": "string"
},
"description": "US only, and only when countryCodes is exactly [\"US\"]"
},
"contentLabelIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "what the creator posts about: from list_tiktok_creator_labels(labelType:\"SEARCH\")"
},
"maxCreatorPrice": {
"type": "number",
"description": "maximum creator starting price in USD"
},
"minCreatorPrice": {
"type": "number",
"description": "the creator’s starting price, USD"
},
"industryLabelIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "commercial categories they have worked in: same source"
},
"maxEngagementRate": {
"type": "number",
"description": "maximum engagement rate as a fraction, 0-1 (0.05 = 5%)"
},
"minEngagementRate": {
"type": "number",
"description": "0-1, NOT a percentage"
},
"followerGenderRatio": {
"enum": [
"FEMALE_50",
"FEMALE_60",
"FEMALE_70",
"MALE_50",
"MALE_60",
"MALE_70"
],
"type": "string",
"description": "audience gender split bucket, e.g. FEMALE_60 or MALE_50"
},
"followerCountryCodes": {
"type": "array",
"items": {
"type": "string"
},
"description": "where their AUDIENCE is, which is often not where they are"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"region"
],
"properties": {
"max": {
"type": "number",
"description": "rows to return, default 200, cap 500. The `total` field always reports how many the file actually held."
},
"adId": {
"type": "string",
"description": "just that ad’s leads. Pass this OR pageId, never both — they are different scopes and TikTok would pick for you."
},
"pageId": {
"type": "string",
"description": "every lead on one Instant Form, from list_tiktok_ads_lead_forms"
},
"region": {
"type": "string",
"description": "REQUIRED. \"us\" = the United States · \"eu\" = the EEA, Switzerland and the UK · \"other\" = every country EXCEPT those — \"other\" is a third bucket, NOT \"everywhere\". Check the ad group’s targeting if unsure."
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}| string |
| e.g. PURCHASE — used with pixelId |
| optimizationGoal | string | override the goal, e.g. REACH / LINK_CLICKS / OFFSITE_CONVERSIONS |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"adSetId": {
"type": "string",
"description": "size an EXISTING ad set using its own saved targeting"
},
"country": {
"type": "string",
"description": "2-letter fallback country when targeting names no geo"
},
"pixelId": {
"type": "string",
"description": "estimate a conversion goal against this pixel"
},
"objective": {
"type": "string",
"description": "OUTCOME_TRAFFIC | OUTCOME_SALES | … — picks the matching optimization goal"
},
"targeting": {
"type": "object",
"description": "a targeting object, same shape as create_meta_ad.targeting",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "ad account id (act_… or digits)"
},
"conversionEvent": {
"type": "string",
"description": "e.g. PURCHASE — used with pixelId"
},
"optimizationGoal": {
"type": "string",
"description": "override the goal, e.g. REACH / LINK_CLICKS / OFFSITE_CONVERSIONS"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"type"
],
"properties": {
"q": {
"type": "string",
"description": "what to search for"
},
"type": {
"enum": [
"adinterest",
"adTargetingCategory",
"adgeolocation",
"adlocale",
"adcountry",
"adzipcode",
"adeducationschool",
"adeducationmajor",
"adworkemployer",
"adworkposition"
],
"type": "string",
"description": "what kind of targeting object to search"
},
"class": {
"type": "string",
"description": "for adTargetingCategory, e.g. \"behaviors\" or \"interests\""
},
"limit": {
"type": "number",
"description": "maximum matches to return; the reply shows up to 25"
},
"adAccountId": {
"type": "string",
"description": "search with that ad account’s token"
},
"countryCode": {
"type": "string",
"description": "2-letter hint to disambiguate a city name"
},
"locationTypes": {
"type": "string",
"description": "comma-separated: country,region,city,zip,geo_market"
}
}
}| X ad account id (list_x_ads_accounts); omit if only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"days": {
"type": "number",
"description": "window length when startTime is omitted; default 30"
},
"format": {
"enum": [
"json",
"csv",
"xlsx"
],
"type": "string",
"description": "json (default, returns leads inline) | csv | xlsx (returns a download link)"
},
"endTime": {
"type": "string",
"description": "ISO 8601; default now"
},
"exportId": {
"type": "string",
"description": "collect an export that was still running"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"startTime": {
"type": "string",
"description": "ISO 8601"
},
"leadFormId": {
"type": "string",
"description": "X lead form id from list_x_ads_lead_forms; required unless exportId is given"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}| customerId | string | 10-digit account id — omit to use the brand’s selected default account |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "rows, max 10000 — Google’s own ceiling, and the clamp is reported"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD, default 14 days ago"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD, default today"
},
"detail": {
"type": "boolean",
"description": "change_event only — include the old/new resource snapshots"
},
"source": {
"enum": [
"change_event",
"change_status"
],
"type": "string",
"description": "default change_event (30 days, field-level). change_status is 90 days and is the only one that sees Google Ads Editor."
},
"customerId": {
"type": "string",
"description": "10-digit account id — omit to use the brand’s selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
}
}
}| customerId |
| string |
| 10-digit account id (dashes ok) — omit to use the brand’s selected default account |
| countryCode | string | 2-letter hint to disambiguate a city name |
| loginCustomerId | string | manager id if operating through an MCC |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "one page to mine for ideas"
},
"site": {
"type": "string",
"description": "a whole domain to mine, e.g. example.com"
},
"limit": {
"type": "number",
"description": "how many ideas to return (1–200, default 50)"
},
"network": {
"enum": [
"GOOGLE_SEARCH",
"GOOGLE_SEARCH_AND_PARTNERS"
],
"type": "string",
"description": "default GOOGLE_SEARCH"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 20 seed terms"
},
"language": {
"type": "string",
"description": "ISO code, e.g. \"en\""
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "place NAMES, e.g. [\"United States\"]"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"countryCode": {
"type": "string",
"description": "2-letter hint to disambiguate a city name"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}| adAccountId | string | LinkedIn ad account id from list_linkedin_ads_campaigns |
| campaignIds | array | limit the report to these campaign ids; omit for all |
| granularity | string | default ALL |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"pivot": {
"type": "string",
"description": "CAMPAIGN (default), CAMPAIGN_GROUP, CREATIVE, ACCOUNT, CONVERSION, PLACEMENT_NAME, IMPRESSION_DEVICE_TYPE, SERVING_LOCATION, or one MEMBER_* demographic pivot — an unknown value is refused with the full list"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD, default 30 days ago"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD"
},
"fields": {
"type": "array",
"items": {
"type": "string"
},
"description": "metric names — omit for the standard set (LinkedIn returns ONLY impressions and clicks if none are named)"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id from list_linkedin_ads_campaigns"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "limit the report to these campaign ids; omit for all"
},
"granularity": {
"enum": [
"ALL",
"DAILY",
"MONTHLY",
"YEARLY"
],
"type": "string",
"description": "default ALL"
}
}
}| countryCode |
| string |
| two-letter country code for the pricing market, e.g. US |
| dailyBudget | number | daily budget to check against LinkedIn's limits, in the account currency |
| campaignType | string | default SPONSORED_UPDATES |
| objectiveType | string | optional — LinkedIn prices some objective/optimization combinations and not others |
| targetingCriteria | object | raw LinkedIn targeting object; overrides locations and include |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bidType": {
"enum": [
"CPM",
"CPC",
"CPV"
],
"type": "string",
"description": "default CPM"
},
"include": {
"type": "object",
"description": "targeting facets ANDed in, e.g. {titles, industries}; same shape as create_linkedin_ads_campaign include",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"currency": {
"type": "string",
"description": "ISO currency code for the estimate, e.g. USD"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "geo URNs or ids for the audience, from search_linkedin_ads_targeting"
},
"matchType": {
"enum": [
"EXACT",
"AUDIENCE_EXPANDED"
],
"type": "string",
"description": "default EXACT"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id; its currency is the default for the estimate"
},
"countryCode": {
"type": "string",
"description": "two-letter country code for the pricing market, e.g. US"
},
"dailyBudget": {
"type": "number",
"description": "daily budget to check against LinkedIn's limits, in the account currency"
},
"campaignType": {
"enum": [
"TEXT_AD",
"SPONSORED_UPDATES",
"SPONSORED_INMAILS"
],
"type": "string",
"description": "default SPONSORED_UPDATES"
},
"objectiveType": {
"type": "string",
"description": "optional — LinkedIn prices some objective/optimization combinations and not others"
},
"targetingCriteria": {
"type": "object",
"description": "raw LinkedIn targeting object; overrides locations and include",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}| includeDeleted |
| boolean |
| true = include deleted ads |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Fetch one ad."
},
"limit": {
"type": "number",
"description": "maximum number of ads to return"
},
"offset": {
"type": "number",
"description": "ads to skip, for paging"
},
"status": {
"enum": [
"ENABLED",
"PAUSED"
],
"type": "string",
"description": "advertiser's own setting: ENABLED or PAUSED"
},
"adGroupId": {
"type": "string",
"description": "only ads in this ad group"
},
"campaignId": {
"type": "string",
"description": "only ads in this campaign"
},
"creativeId": {
"type": "string",
"description": "only ads that use this creative"
},
"includeDeleted": {
"type": "boolean",
"description": "true = include deleted ads"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Fetch one asset — the only way to see a crop or variant."
},
"limit": {
"type": "number",
"description": "maximum number of assets to return"
},
"offset": {
"type": "number",
"description": "assets to skip, for paging"
},
"brandId": {
"type": "string",
"description": "older name for mapsBrandId (MCP only)"
},
"assetType": {
"enum": [
"IMAGE"
],
"type": "string",
"description": "asset type; IMAGE is the only one listed"
},
"mapsBrandId": {
"type": "string",
"description": "Scope to one brand or app (promotedObjectId)."
},
"promotedObjectType": {
"enum": [
"APPSTORE_APP",
"BUSINESS_BRAND"
],
"type": "string",
"description": "APPSTORE_APP or BUSINESS_BRAND: which kind of promoted object the assets belong to"
}
}
}| string |
| Usually ELIGIBLE — only an ELIGIBLE brand can be used in a campaign. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "brand name to filter by"
},
"limit": {
"type": "number",
"description": "maximum number of brands to return"
},
"offset": {
"type": "number",
"description": "brands to skip, for paging"
},
"brandId": {
"type": "string",
"description": "older name for mapsBrandId (MCP only)"
},
"mapsBrandId": {
"type": "string",
"description": "Fetch one brand by id."
},
"countryOrRegion": {
"type": "string",
"description": "Two-letter code, e.g. \"US\"."
},
"eligibilityStatus": {
"type": "string",
"description": "Usually ELIGIBLE — only an ELIGIBLE brand can be used in a campaign."
}
}
}| includeDeleted | boolean | true = include deleted creatives, which Apple hides by default |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Fetch one creative."
},
"name": {
"type": "string",
"description": "Name prefix."
},
"limit": {
"type": "number",
"description": "maximum number of creatives to return"
},
"offset": {
"type": "number",
"description": "creatives to skip, for paging"
},
"eligibility": {
"enum": [
"ELIGIBLE",
"INELIGIBLE"
],
"type": "string",
"description": "ELIGIBLE or INELIGIBLE for ad placement"
},
"creativeType": {
"enum": [
"DEFAULT_PRODUCT_PAGE",
"CUSTOM_PRODUCT_PAGE",
"LOCAL_ADS_SEARCH_CREATIVE"
],
"type": "string",
"description": "creative kind: DEFAULT_PRODUCT_PAGE, CUSTOM_PRODUCT_PAGE or LOCAL_ADS_SEARCH_CREATIVE"
},
"systemStatus": {
"enum": [
"VALID",
"INVALID",
"PENDING"
],
"type": "string",
"description": "Apple's review state: VALID, INVALID or PENDING"
},
"includeDeleted": {
"type": "boolean",
"description": "true = include deleted creatives, which Apple hides by default"
}
}
}| campaignId |
| string |
| the campaign — required unless you pass adGroupId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of keywords to return"
},
"scope": {
"enum": [
"campaign",
"adgroup"
],
"type": "string",
"description": "NEGATIVE keywords only, with campaignId and no adGroupId: \"campaign\" (default) = the campaign-level negatives; \"adgroup\" = every ad-group-level negative across the campaign."
},
"offset": {
"type": "number",
"description": "keywords to skip, for paging"
},
"negative": {
"type": "boolean",
"description": "list negative keywords instead of targeting keywords"
},
"adGroupId": {
"type": "string",
"description": "narrow to one ad group. Outranks scope on both resources."
},
"campaignId": {
"type": "string",
"description": "the campaign — required unless you pass adGroupId"
}
}
}| includeDeleted | boolean | Deleted groups are hidden by default. |
| eligibilityStatus | string | filter by Apple eligibility state of the group |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Fetch one group."
},
"name": {
"type": "string",
"description": "Substring match."
},
"limit": {
"type": "number",
"description": "maximum number of location groups to return"
},
"offset": {
"type": "number",
"description": "groups to skip, for paging"
},
"brandId": {
"type": "string",
"description": "older name for mapsBrandId (MCP only)"
},
"groupType": {
"enum": [
"STATIC",
"DYNAMIC"
],
"type": "string",
"description": "STATIC (fixed shop list) or DYNAMIC (rule-based)"
},
"mapsBrandId": {
"type": "string",
"description": "Apple Maps brand id from list_apple_ads_brands"
},
"includeDeleted": {
"type": "boolean",
"description": "Deleted groups are hidden by default."
},
"eligibilityStatus": {
"type": "string",
"description": "filter by Apple eligibility state of the group"
}
}
}| string |
| City name. |
| adminArea | string | Full state or province name, e.g. "California". |
| postalCode | string | postal code to filter by |
| mapsBrandId | string | Strongly recommended — scopes to one brand. |
| countryOrRegion | string | Two-letter code. |
| eligibilityStatus | string | Apple eligibility state; only ELIGIBLE locations can be targeted |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "Fetch one location."
},
"name": {
"type": "string",
"description": "location name to filter by"
},
"limit": {
"type": "number",
"description": "maximum number of locations to return"
},
"offset": {
"type": "number",
"description": "locations to skip, for paging"
},
"status": {
"enum": [
"OPEN",
"OPENING_SOON",
"CLOSED",
"MOVED",
"TEMPORARILY_CLOSED"
],
"type": "string",
"description": "Usually OPEN — only OPEN locations are targetable."
},
"brandId": {
"type": "string",
"description": "older name for mapsBrandId (MCP only)"
},
"locality": {
"type": "string",
"description": "City name."
},
"adminArea": {
"type": "string",
"description": "Full state or province name, e.g. \"California\"."
},
"postalCode": {
"type": "string",
"description": "postal code to filter by"
},
"mapsBrandId": {
"type": "string",
"description": "Strongly recommended — scopes to one brand."
},
"countryOrRegion": {
"type": "string",
"description": "Two-letter code."
},
"eligibilityStatus": {
"enum": [
"ELIGIBLE",
"INELIGIBLE",
"LIMITED",
"PENDING",
"UNDEFINED"
],
"type": "string",
"description": "Apple eligibility state; only ELIGIBLE locations can be targeted"
}
}
}| localeDetails | boolean | Read per-language content instead of the page list. |
| productPageId | string | A specific custom product page (UUID). |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "maximum number of rows to return"
},
"state": {
"type": "string",
"description": "Filter by page state; the typical live value is PUBLISHED."
},
"adamId": {
"type": "string",
"description": "App Store id — one app’s pages, or its DEFAULT listing’s locale details."
},
"offset": {
"type": "number",
"description": "rows to skip, for paging"
},
"language": {
"type": "string",
"description": "With localeDetails — a language, e.g. \"en\"."
},
"languageCode": {
"type": "string",
"description": "With localeDetails — one locale, e.g. \"en-US\". Omit for all."
},
"localeDetails": {
"type": "boolean",
"description": "Read per-language content instead of the page list."
},
"productPageId": {
"type": "string",
"description": "A specific custom product page (UUID)."
}
}
}| string |
| Default APPSTORE_APP. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"promotedObjectId"
],
"properties": {
"type": {
"enum": [
"daily_budget",
"target_cpa"
],
"type": "string",
"description": "Default daily_budget."
},
"limit": {
"type": "number",
"description": "maximum number of recommendations to return"
},
"state": {
"type": "string",
"description": "AVAILABLE (default), APPLIED, DISMISSED, DELETE, or \"ALL\" to skip the filter."
},
"offset": {
"type": "number",
"description": "recommendations to skip, for paging"
},
"promotedObjectId": {
"type": "string",
"description": "REQUIRED — the app’s adamId, or an Apple Maps brandId."
},
"promotedObjectType": {
"enum": [
"APPSTORE_APP",
"BUSINESS_BRAND"
],
"type": "string",
"description": "Default APPSTORE_APP."
}
}
}| include trashed files (default false) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"query": {
"type": "string",
"description": "only files whose name contains this"
},
"folderId": {
"type": "string",
"description": "list the contents of this folder id"
},
"pageSize": {
"type": "number",
"description": "rows per page (1–200, default 50)"
},
"pageToken": {
"type": "string",
"description": "cursor from a previous call"
},
"recursive": {
"type": "boolean",
"description": "with folderId: the whole tree"
},
"onlyFolders": {
"type": "boolean",
"description": "list folders only"
},
"includeTrashed": {
"type": "boolean",
"description": "include trashed files (default false)"
}
}
}| string |
| manager (MCC) id — only if reaching a client account through a manager |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "max campaigns (1–500, default 100)"
},
"since": {
"type": "string",
"description": "start date YYYY-MM-DD (with until)"
},
"until": {
"type": "string",
"description": "end date YYYY-MM-DD"
},
"status": {
"enum": [
"ENABLED",
"PAUSED",
"REMOVED"
],
"type": "string",
"description": "filter campaigns by status"
},
"metrics": {
"type": "boolean",
"description": "include performance metrics (default true)"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to list accessible accounts"
},
"datePreset": {
"type": "string",
"description": "metrics window preset (default LAST_30_DAYS)"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) id — only if reaching a client account through a manager"
}
}
}| lifecycleStage | string | internal value, e.g. customer, opportunity, salesqualifiedlead, lead |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"after": {
"type": "string",
"description": "pagination cursor from the previous page"
},
"limit": {
"type": "number",
"description": "1–100, default 25"
},
"search": {
"type": "string",
"description": "name, domain, website or phone"
},
"ownerId": {
"type": "string",
"description": "older name for hubspotOwnerId (MCP only)"
},
"industry": {
"type": "string",
"description": "HubSpot's internal industry value, e.g. COMPUTER_SOFTWARE"
},
"createdSince": {
"type": "string",
"description": "YYYY-MM-DD"
},
"hubspotOwnerId": {
"type": "string",
"description": "owner id from list_hubspot_owners"
},
"lifecycleStage": {
"type": "string",
"description": "internal value, e.g. customer, opportunity, salesqualifiedlead, lead"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"type": {
"enum": [
"general",
"unique",
"average"
],
"type": "string",
"description": "count type for the event amounts: general (default), unique or average"
},
"event": {
"type": "string",
"description": "pass an event name to list ITS properties instead of listing events"
},
"limit": {
"type": "number",
"description": "Mixpanel own defaults are 255 for the vocabulary, 100 for today and 10 for properties"
},
"window": {
"enum": [
"vocabulary",
"today"
],
"type": "string",
"description": "\"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"
}
}
}| only changes to this ad group |
| campaignId | string | include changes to this campaign and its ad groups and ads |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "only changes to this ad"
},
"after": {
"type": "string",
"description": "pagination cursor for the next page"
},
"limit": {
"type": "number",
"description": "1-100, default 25"
},
"order": {
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "asc or desc, by time"
},
"since": {
"type": "string",
"description": "start of the window: date, ISO time or unix seconds"
},
"until": {
"type": "string",
"description": "end of the window: date, ISO time or unix seconds"
},
"before": {
"type": "string",
"description": "pagination cursor for the previous page"
},
"actorId": {
"type": "string",
"description": "only changes made by this actor id"
},
"adGroupId": {
"type": "string",
"description": "only changes to this ad group"
},
"campaignId": {
"type": "string",
"description": "include changes to this campaign and its ad groups and ads"
}
}
}| surveyId | string | one survey: its questions and stats |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1-100, default 25"
},
"dateTo": {
"type": "string",
"description": "stats window end, ISO"
},
"offset": {
"type": "number",
"description": "skip this many surveys; use the nextOffset from the previous reply"
},
"search": {
"type": "string",
"description": "match survey names containing this text"
},
"status": {
"enum": [
"running",
"complete",
"draft"
],
"type": "string",
"description": "only surveys in this state"
},
"archived": {
"type": "boolean",
"description": "PostHog archived filter: true or false; omit to leave it unfiltered"
},
"dateFrom": {
"type": "string",
"description": "stats window start, ISO"
},
"surveyId": {
"type": "string",
"description": "one survey: its questions and stats"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "filter by asset name"
},
"limit": {
"type": "number",
"description": "1–100, default 25"
},
"types": {
"type": "array",
"items": {
"enum": [
"IMAGE",
"VIDEO"
],
"type": "string"
},
"description": "Reddit lists media only"
},
"postId": {
"type": "string",
"description": "a post id (t3_…) from list_reddit_ads_posts: list the assets that post uses, with role and crop, instead of the profile library"
},
"pageToken": {
"type": "string",
"description": "nextPageToken from the previous page"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_…) — omit when only one is shared"
},
"redditProfileId": {
"type": "string",
"description": "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"
}
}
}| PENDING, APPROVED or REJECTED: filter by Reddit review state |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "specific product ids to read"
},
"limit": {
"type": "number",
"description": "page size, default 50, max 1000"
},
"search": {
"type": "string",
"description": "text to match against product fields"
},
"catalogId": {
"type": "string",
"description": "one of catalogId or productSetId"
},
"pageToken": {
"type": "string",
"description": "nextPageToken from the previous page"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"productSetId": {
"type": "string",
"description": "list the products in this product set instead of the whole catalog"
},
"approvalDecision": {
"enum": [
"PENDING",
"APPROVED",
"REJECTED"
],
"type": "string",
"description": "PENDING, APPROVED or REJECTED: filter by Reddit review state"
}
}
}| default IN_APP_EVENT |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"appId"
],
"properties": {
"appId": {
"type": "string",
"description": "from list_tiktok_ads_apps"
},
"objective": {
"type": "string",
"description": "default APP_INSTALL"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"availableOnly": {
"type": "boolean",
"description": "default true at TikTok — false also returns events that are not yet usable"
},
"placementType": {
"enum": [
"PLACEMENT_TYPE_AUTOMATIC",
"PLACEMENT_TYPE_NORMAL"
],
"type": "string",
"description": "default PLACEMENT_TYPE_AUTOMATIC"
},
"appPromotionType": {
"enum": [
"APP_INSTALL",
"APP_RETARGETING"
],
"type": "string",
"description": "APP_INSTALL or APP_RETARGETING; omit for TikTok's default"
},
"optimizationGoal": {
"type": "string",
"description": "default IN_APP_EVENT"
}
}
}| up to 100, from list_tiktok_ads_catalog_sets |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId"
],
"properties": {
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers"
},
"page": {
"type": "number",
"description": "page number, from 1"
},
"skuIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 1,000 — not together with productIds"
},
"pageSize": {
"type": "number",
"description": "up to 500"
},
"catalogId": {
"type": "string",
"description": "from list_tiktok_ads_catalogs"
},
"productIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 1,000 — not together with skuIds"
},
"productSetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 100, from list_tiktok_ads_catalog_sets"
}
}
}| REQUIRED — from list_tiktok_ads_campaigns. TikTok offers no other way to scope a comment read. |
| sortField | string | default CREATE_TIME |
| startTime* | string | YYYY-MM-DD. At most 30 days before endTime — TikTok refuses a wider window. |
| advertiserId | string | from list_tiktok_ads_accounts — omit only when exactly one is reachable |
| commentTypes | array | default ALL. COMMENT is a top-level comment, REPLY is an answer to one. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adgroupId",
"startTime",
"endTime"
],
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"endTime": {
"type": "string",
"description": "YYYY-MM-DD"
},
"pageSize": {
"type": "number",
"description": "up to 100"
},
"sortType": {
"enum": [
"ASC",
"DESC"
],
"type": "string",
"description": "default DESC"
},
"adgroupId": {
"type": "string",
"description": "REQUIRED — from list_tiktok_ads_campaigns. TikTok offers no other way to scope a comment read."
},
"sortField": {
"enum": [
"CREATE_TIME",
"LIKES",
"REPLIES"
],
"type": "string",
"description": "default CREATE_TIME"
},
"startTime": {
"type": "string",
"description": "YYYY-MM-DD. At most 30 days before endTime — TikTok refuses a wider window."
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit only when exactly one is reachable"
},
"commentTypes": {
"type": "array",
"items": {
"enum": [
"ALL",
"COMMENT",
"REPLY"
],
"type": "string"
},
"description": "default ALL. COMMENT is a top-level comment, REPLY is an answer to one."
}
}
}| identityType* | string | REQUIRED — the matching type from list_tiktok_ads_identities. Only AUTH_CODE, TT_USER and BC_AUTH_TT can front a Spark Ad; CUSTOMIZED_USER cannot and is refused here. |
| excludeAdsOnly | boolean | true hides ads-only posts (ones published as ads and hidden from the profile), returning at most the 500 most recent organic posts |
| identityAuthorizedBcId | string | REQUIRED when identityType is BC_AUTH_TT — reported next to the id by list_tiktok_ads_identities |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"identityId",
"identityType"
],
"properties": {
"count": {
"type": "number",
"description": "1-20; TikTok ignores anything larger and returns 20"
},
"cursor": {
"type": "string",
"description": "from the previous call, while hasMore is true"
},
"itemType": {
"enum": [
"VIDEO",
"CAROUSEL"
],
"type": "string",
"description": "TikTok defaults to VIDEO only — pass CAROUSEL for photo posts"
},
"identityId": {
"type": "string",
"description": "REQUIRED — from list_tiktok_ads_identities"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"identityType": {
"type": "string",
"description": "REQUIRED — the matching type from list_tiktok_ads_identities. Only AUTH_CODE, TT_USER and BC_AUTH_TT can front a Spark Ad; CUSTOMIZED_USER cannot and is refused here."
},
"excludeAdsOnly": {
"type": "boolean",
"description": "true hides ads-only posts (ones published as ads and hidden from the profile), returning at most the 500 most recent organic posts"
},
"identityAuthorizedBcId": {
"type": "string",
"description": "REQUIRED when identityType is BC_AUTH_TT — reported next to the id by list_tiktok_ads_identities"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"scene": {
"enum": [
"CAROUSEL_ADS",
"CREATIVE_ASSET"
],
"type": "string",
"description": "CAROUSEL_ADS when searching; CREATIVE_ASSET for the creative-tools library"
},
"keyword": {
"type": "string",
"description": "search text for the music library"
},
"musicIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "music ids to look up"
},
"pageSize": {
"type": "number",
"description": "rows to fetch from TikTok; the reply shows the first 30"
},
"imageUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-35 URLs of images already uploaded to the ad account"
},
"searchType": {
"enum": [
"SEARCH_BY_KEYWORD",
"SEARCH_BY_RECOMMEND",
"SEARCH_BY_LIKED",
"SEARCH_BY_HISTORY",
"SEARCH_BY_MUSIC_ID",
"SEARCH_BY_SOURCE"
],
"type": "string",
"description": "SEARCH_BY_KEYWORD, SEARCH_BY_RECOMMEND, SEARCH_BY_LIKED, SEARCH_BY_HISTORY, SEARCH_BY_MUSIC_ID or SEARCH_BY_SOURCE"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers"
},
"page": {
"type": "number",
"description": "page number, from 1"
},
"type": {
"enum": [
"SHARED",
"NON_SHARED"
],
"type": "string",
"description": "SHARED is Advanced (pooled funds), NON_SHARED is Standard (per-account)"
},
"pageSize": {
"type": "number",
"description": "1-50"
},
"bcRelated": {
"type": "boolean",
"description": "true narrows to portfolios actually linked to this Business Center"
},
"paymentPortfolioIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "filter, max 10"
}
}
}| dimension |
| string |
| level the rule watches: CAMPAIGN, ADGROUP or AD |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"tzone": {
"type": "string",
"description": "timezone string for the rule's time windows; optional"
},
"status": {
"enum": [
"ON",
"OFF",
"DELETED"
],
"type": "string",
"description": "ON, OFF or DELETED; omit for all"
},
"ruleIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "specific rules — omit to list them all"
},
"pageSize": {
"type": "number",
"description": "rows per page"
},
"ruleInfo": {
"type": "array",
"items": {
"type": "string"
},
"description": "rule ids or rule names to filter by"
},
"dimension": {
"enum": [
"CAMPAIGN",
"ADGROUP",
"AD"
],
"type": "string",
"description": "level the rule watches: CAMPAIGN, ADGROUP or AD"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}| advertiserId |
| string |
| REQUIRED when adCreationEligible is given |
| itemGroupIds | array | product SPU ids, max 10 |
| adCreationEligible | string | CUSTOM_SHOP_ADS (Shopping Ads) or GMV_MAX; also needs advertiserId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"storeId"
],
"properties": {
"bcId": {
"type": "string",
"description": "from the store row, or from list_tiktok_ads_business_centers"
},
"page": {
"type": "number",
"description": "page number, from 1"
},
"storeId": {
"type": "string",
"description": "from list_tiktok_ads_stores"
},
"pageSize": {
"type": "number",
"description": "1-100, default 10"
},
"sortType": {
"enum": [
"ASC",
"DESC"
],
"type": "string",
"description": "ASC or DESC"
},
"sortField": {
"enum": [
"min_price",
"historical_sales"
],
"type": "string",
"description": "min_price or historical_sales"
},
"productName": {
"type": "string",
"description": "filter by product title"
},
"advertiserId": {
"type": "string",
"description": "REQUIRED when adCreationEligible is given"
},
"itemGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "product SPU ids, max 10"
},
"adCreationEligible": {
"enum": [
"CUSTOM_SHOP_ADS",
"GMV_MAX"
],
"type": "string",
"description": "CUSTOM_SHOP_ADS (Shopping Ads) or GMV_MAX; also needs advertiserId"
}
}
}| YYYY-MM-DD — a range, with endDate |
| description | string | longer note shown with the annotation (create or update) |
| annotationId | string | for update/delete — from action:'list' |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"date": {
"type": "string",
"description": "YYYY-MM-DD — one day"
},
"color": {
"enum": [
"PURPLE",
"BROWN",
"BLUE",
"GREEN",
"RED",
"CYAN",
"ORANGE"
],
"type": "string",
"description": "chart colour; default BLUE"
},
"title": {
"type": "string",
"description": "the label on the chart. Required to create"
},
"action": {
"enum": [
"list",
"create",
"update",
"delete"
],
"type": "string",
"description": "defaults to 'list'"
},
"confirm": {
"type": "boolean",
"description": "must be true to actually delete"
},
"endDate": {
"type": "string",
"description": "last day of a range, YYYY-MM-DD; use with startDate instead of date"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id, from list_analytics_properties"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD — a range, with endDate"
},
"description": {
"type": "string",
"description": "longer note shown with the annotation (create or update)"
},
"annotationId": {
"type": "string",
"description": "for update/delete — from action:'list'"
}
}
}| description | string | max 4096 characters |
| displayName | string | the name shown in GA4, max 82 characters. Required to create |
| calculatedMetricId | string | the API name, e.g. "cost_per_purchase". Required to create and PERMANENT; for update/delete it names the metric |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"action": {
"enum": [
"list",
"create",
"update",
"delete"
],
"type": "string",
"description": "defaults to 'list'"
},
"confirm": {
"type": "boolean",
"description": "must be true to actually delete"
},
"formula": {
"type": "string",
"description": "e.g. \"advertiserAdCost / conversions\". Required to create"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id, from list_analytics_properties"
},
"metricUnit": {
"type": "string",
"description": "STANDARD | CURRENCY | FEET | MILES | METERS | KILOMETERS | MILLISECONDS | SECONDS | MINUTES | HOURS. Required to create"
},
"description": {
"type": "string",
"description": "max 4096 characters"
},
"displayName": {
"type": "string",
"description": "the name shown in GA4, max 82 characters. Required to create"
},
"calculatedMetricId": {
"type": "string",
"description": "the API name, e.g. \"cost_per_purchase\". Required to create and PERMANENT; for update/delete it names the metric"
}
}
}| channels |
| array |
| ordered list of {name, conditions:[…]}. Required to create. On update it REPLACES every channel, so send the ones you want to keep |
| property* | string | NUMERIC GA4 property id, from list_analytics_properties |
| description | string | max 256 characters |
| displayName | string | the group name shown in GA4, max 80 characters. Required to create; may also name an existing group for update/delete |
| channelGroupId | string | for update and delete — from action:'list', and unambiguous where a name is not |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"action": {
"enum": [
"list",
"create",
"update",
"delete"
],
"type": "string",
"description": "defaults to 'list'"
},
"confirm": {
"type": "boolean",
"description": "must be true to actually delete"
},
"primary": {
"type": "boolean",
"description": "make this the group reports are bucketed by. It unsets the previous primary group, changing what every report in the property shows"
},
"channels": {
"type": "array",
"items": {},
"description": "ordered list of {name, conditions:[…]}. Required to create. On update it REPLACES every channel, so send the ones you want to keep"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id, from list_analytics_properties"
},
"description": {
"type": "string",
"description": "max 256 characters"
},
"displayName": {
"type": "string",
"description": "the group name shown in GA4, max 80 characters. Required to create; may also name an existing group for update/delete"
},
"channelGroupId": {
"type": "string",
"description": "for update and delete — from action:'list', and unambiguous where a name is not"
}
}
}| stream id, measurement id or display name. Optional when the property has exactly one stream; required when it has several, and an ambiguous name is refused rather than first-matched |
| displayName | string | REQUIRED for action:'create' — what will be sending with this secret, e.g. "Shopify webhook". It is the only label a secret ever gets. May also be used to name one for delete |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"property"
],
"properties": {
"action": {
"enum": [
"list",
"create",
"delete"
],
"type": "string",
"description": "defaults to 'list'"
},
"confirm": {
"type": "boolean",
"description": "must be true to actually delete"
},
"property": {
"type": "string",
"description": "NUMERIC GA4 property id"
},
"secretId": {
"type": "string",
"description": "for action:'delete', from action:'list' — preferred over displayName because it is unambiguous"
},
"dataStream": {
"type": "string",
"description": "stream id, measurement id or display name. Optional when the property has exactly one stream; required when it has several, and an ambiguous name is refused rather than first-matched"
},
"displayName": {
"type": "string",
"description": "REQUIRED for action:'create' — what will be sending with this secret, e.g. \"Shopify webhook\". It is the only label a secret ever gets. May also be used to name one for delete"
}
}
}| update — the asset kind |
| fieldType | string | the link field type from list, e.g. CALL, SITELINK |
| campaignId | string | campaign id, when level is campaign |
| customerId | string | 10-digit account id; omit to use the brand's selected default account |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"asset": {
"type": "object",
"description": "update — the new content",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"level": {
"enum": [
"campaign",
"adGroup",
"customer"
],
"type": "string",
"description": "campaign, adGroup or customer; follows the id you pass"
},
"action": {
"enum": [
"list",
"update",
"pause",
"enable",
"unlink"
],
"type": "string",
"description": "default list"
},
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and change nothing"
},
"assetId": {
"type": "string",
"description": "asset id, from the list action"
},
"confirm": {
"type": "boolean",
"description": "true only after the user agreed to change a LIVE campaign's assets"
},
"adGroupId": {
"type": "string",
"description": "ad group id, when level is adGroup"
},
"assetType": {
"type": "string",
"description": "update — the asset kind"
},
"fieldType": {
"type": "string",
"description": "the link field type from list, e.g. CALL, SITELINK"
},
"campaignId": {
"type": "string",
"description": "campaign id, when level is campaign"
},
"customerId": {
"type": "string",
"description": "10-digit account id; omit to use the brand's selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
}
}
}| full http(s) URLs of sites like yours for a custom segment (create_custom_segment) |
| query | string | search text matched against audience and interest names (action search) |
| action | string | default search |
| dryRun | boolean | true = validate with Google and create or remove nothing |
| confirm | boolean | true to actually remove the list or segment (action remove only) |
| genders | array | genders to include: male, female or unknown |
| keywords | array | search phrases for a custom segment, 10 words or fewer each (create_custom_segment) |
| urlRules | array | visitor rules for create_remarketing_list: [{operator, value}]; visitors to pages matching any rule |
| interests | array | interest ids (affinity or in-market) to include in an audience, from action search |
| userLists | array | user list ids to include in an audience (create_audience), from action search |
| customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's selected default account |
| description | string | optional description (create actions) |
| customSegments | array | custom segment ids to include in an audience, from action search |
| membershipDays | number | 1–540, default 30 |
| excludeUrlRules | array | same shape as urlRules; visitors to matching pages are left out of the list |
| loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "remove"
},
"ages": {
"type": "array",
"items": {
"type": "string"
},
"description": "age ranges to include: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown"
},
"apps": {
"type": "array",
"items": {
"type": "string"
},
"description": "app identifiers for a custom segment (create_custom_segment)"
},
"kind": {
"enum": [
"userList",
"customSegment"
],
"type": "string",
"description": "remove"
},
"name": {
"type": "string",
"description": "audience or list name, unique on the account (create actions)"
},
"type": {
"enum": [
"AUTO",
"SEARCH"
],
"type": "string",
"description": "custom segment type: AUTO (Google decides, default) or SEARCH (people who searched the keywords)"
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "full http(s) URLs of sites like yours for a custom segment (create_custom_segment)"
},
"query": {
"type": "string",
"description": "search text matched against audience and interest names (action search)"
},
"action": {
"enum": [
"search",
"create_remarketing_list",
"create_custom_segment",
"create_audience",
"remove"
],
"type": "string",
"description": "default search"
},
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and create or remove nothing"
},
"confirm": {
"type": "boolean",
"description": "true to actually remove the list or segment (action remove only)"
},
"genders": {
"type": "array",
"items": {
"enum": [
"male",
"female",
"unknown"
],
"type": "string"
},
"description": "genders to include: male, female or unknown"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "search phrases for a custom segment, 10 words or fewer each (create_custom_segment)"
},
"urlRules": {
"type": "array",
"items": {
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"type": "string",
"description": "URL fragment to match, e.g. /pricing; no quotes, tabs, newlines or parentheses"
},
"operator": {
"type": "string",
"description": "CONTAINS (default), EQUALS, STARTS_WITH or ENDS_WITH"
}
}
},
"description": "visitor rules for create_remarketing_list: [{operator, value}]; visitors to pages matching any rule"
},
"interests": {
"type": "array",
"items": {
"type": "string"
},
"description": "interest ids (affinity or in-market) to include in an audience, from action search"
},
"userLists": {
"type": "array",
"items": {
"type": "string"
},
"description": "user list ids to include in an audience (create_audience), from action search"
},
"customerId": {
"type": "string",
"description": "Google Ads account id, 10 digits (dashes ok); omit for the brand's selected default account"
},
"description": {
"type": "string",
"description": "optional description (create actions)"
},
"customSegments": {
"type": "array",
"items": {
"type": "string"
},
"description": "custom segment ids to include in an audience, from action search"
},
"membershipDays": {
"type": "number",
"description": "1–540, default 30"
},
"excludeUrlRules": {
"type": "array",
"items": {
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"type": "string",
"description": "URL fragment to exclude, same rules as urlRules[].value"
},
"operator": {
"type": "string",
"description": "CONTAINS (default), EQUALS, STARTS_WITH or ENDS_WITH"
}
}
},
"description": "same shape as urlRules; visitors to matching pages are left out of the list"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id, when the account is managed through one"
}
}
}| string |
| omit to use the selected default account |
| targetRoas | number | 4 = 4x revenue per unit spent |
| campaignIds | array | campaign ids to attach to the strategy |
| targetCpaUsd | number | cost per conversion |
| loginCustomerId | string | manager id if operating through an MCC |
| biddingStrategyId | string | the strategy (from action list) |
| impressionSharePercent | number | 1-100 |
| impressionShareLocation | string | where to aim impression share: ANYWHERE_ON_PAGE, TOP_OF_PAGE or ABSOLUTE_TOP_OF_PAGE |
| allowNoConversionTracking | boolean | skip the conversion-tracking check |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "strategy name, for create or update"
},
"action": {
"enum": [
"list",
"create",
"update",
"attach",
"delete"
],
"type": "string",
"description": "default list"
},
"dryRun": {
"type": "boolean",
"description": "validate with Google, change nothing"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved a change to a LIVE campaign or a delete"
},
"strategy": {
"enum": [
"MAXIMIZE_CLICKS",
"MAXIMIZE_CONVERSIONS",
"MAXIMIZE_CONVERSION_VALUE",
"TARGET_CPA",
"TARGET_ROAS",
"TARGET_IMPRESSION_SHARE"
],
"type": "string",
"description": "create"
},
"maxCpcUsd": {
"type": "number",
"description": "max CPC ceiling (required for TARGET_IMPRESSION_SHARE)"
},
"minCpcUsd": {
"type": "number",
"description": "min CPC floor"
},
"customerId": {
"type": "string",
"description": "omit to use the selected default account"
},
"targetRoas": {
"type": "number",
"description": "4 = 4x revenue per unit spent"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "campaign ids to attach to the strategy"
},
"targetCpaUsd": {
"type": "number",
"description": "cost per conversion"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"biddingStrategyId": {
"type": "string",
"description": "the strategy (from action list)"
},
"impressionSharePercent": {
"type": "number",
"description": "1-100"
},
"impressionShareLocation": {
"enum": [
"ANYWHERE_ON_PAGE",
"TOP_OF_PAGE",
"ABSOLUTE_TOP_OF_PAGE"
],
"type": "string",
"description": "where to aim impression share: ANYWHERE_ON_PAGE, TOP_OF_PAGE or ABSOLUTE_TOP_OF_PAGE"
},
"allowNoConversionTracking": {
"type": "boolean",
"description": "skip the conversion-tracking check"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"action": {
"enum": [
"read",
"set",
"clear"
],
"type": "string",
"description": "default 'read'"
},
"dryRun": {
"type": "boolean",
"description": "validate with Google, write nothing"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true for set and clear, after the user approved"
},
"budgetId": {
"type": "string",
"description": "numeric campaign budget id; omit for the account default"
},
"settings": {
"type": "array",
"items": {
"type": "object",
"required": [
"channelType"
],
"properties": {
"caps": {
"type": "array",
"items": {
"type": "object",
"required": [
"channel",
"percent"
],
"properties": {
"channel": {
"type": "string",
"description": "a channel in tier2"
},
"percent": {
"type": "number",
"description": "most of the budget it may spend, 0-100"
}
}
},
"description": "spend caps on tier2 channels"
},
"tier1": {
"type": "array",
"items": {
"enum": [
"OWNED_AND_OPERATED",
"SEARCH_PARTNER",
"GOOGLE_DISPLAY"
],
"type": "string"
},
"description": "channels that spend first"
},
"tier2": {
"type": "array",
"items": {
"enum": [
"OWNED_AND_OPERATED",
"SEARCH_PARTNER",
"GOOGLE_DISPLAY"
],
"type": "string"
},
"description": "channels that get what is left"
},
"channelType": {
"type": "string",
"description": "campaign type: SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX, DEMAND_GEN and so on"
}
}
},
"description": "for set: one entry per campaign type"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok); omit to use the brand’s selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}| customerId |
| string |
| omit to use the selected default account |
| campaignIds | array | campaign ids to apply or remove the label on |
| description | string | up to 200 characters |
| loginCustomerId | string | manager id if operating through an MCC |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ads": {
"type": "array",
"items": {
"type": "object",
"required": [
"adGroupId",
"adId"
],
"properties": {
"adId": {
"type": "string",
"description": "ad id to label"
},
"adGroupId": {
"type": "string",
"description": "ad group the ad belongs to"
}
}
},
"description": "ads to label, as a list of {adGroupId, adId} objects"
},
"name": {
"type": "string",
"description": "label name, for create or update"
},
"color": {
"type": "string",
"description": "#RRGGBB"
},
"action": {
"enum": [
"list",
"create",
"update",
"apply",
"remove",
"delete"
],
"type": "string",
"description": "default list"
},
"dryRun": {
"type": "boolean",
"description": "validate with Google, change nothing"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved a change to a LIVE campaign or a delete"
},
"labelId": {
"type": "string",
"description": "the label (from action list)"
},
"adGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ad group ids to apply or remove the label on"
},
"customerId": {
"type": "string",
"description": "omit to use the selected default account"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "campaign ids to apply or remove the label on"
},
"description": {
"type": "string",
"description": "up to 200 characters"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}| campaign ids to attach to or detach from the shared list |
| sharedSetId | string | the list (from action list) |
| accountLevel | boolean | create: apply to every campaign on the account |
| criterionIds | array | remove_keywords by id |
| loginCustomerId | string | manager id if operating through an MCC |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "create"
},
"action": {
"enum": [
"list",
"create",
"add",
"remove_keywords",
"attach",
"detach",
"delete"
],
"type": "string",
"description": "default list"
},
"dryRun": {
"type": "boolean",
"description": "validate with Google, change nothing"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved a change to a LIVE campaign or a delete"
},
"keywords": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string"
},
"matchType": {
"enum": [
"BROAD",
"PHRASE",
"EXACT"
],
"type": "string"
}
}
}
]
},
"description": "keywords to block, as text strings or {text, matchType BROAD, PHRASE or EXACT} objects"
},
"customerId": {
"type": "string",
"description": "omit to use the selected default account"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "campaign ids to attach to or detach from the shared list"
},
"sharedSetId": {
"type": "string",
"description": "the list (from action list)"
},
"accountLevel": {
"type": "boolean",
"description": "create: apply to every campaign on the account"
},
"criterionIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "remove_keywords by id"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}| loginCustomerId | string | manager id if operating through an MCC |
| recommendationIds | array | apply/dismiss: ids from google_ads_recommendations, max 100; there is no "apply everything" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"action"
],
"properties": {
"action": {
"enum": [
"apply",
"dismiss",
"auto_apply"
],
"type": "string",
"description": "apply (live now; spend types need confirm), dismiss (remove from list), or auto_apply (set auto-apply per type)"
},
"dryRun": {
"type": "boolean",
"description": "validate and change nothing"
},
"confirm": {
"type": "boolean",
"description": "the user saw the list and said yes"
},
"autoApply": {
"type": "object",
"description": "auto_apply: {RECOMMENDATION_TYPE: true|false}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "boolean"
}
},
"customerId": {
"type": "string",
"description": "10-digit account id, omit for the brand’s default account"
},
"confirmCount": {
"type": "number",
"description": "apply: echo the count the unconfirmed call reported"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"recommendationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "apply/dismiss: ids from google_ads_recommendations, max 100; there is no \"apply everything\""
}
}
}| 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 |
| showDeleted | boolean | include archived sources in a list |
| 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 |
| conversionTypes | array | 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 |
| 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 |
| merchantCenterId* | string | from list_merchant_accounts |
| conversionSourceId | string | for update, delete and undelete — from action:'list' |
| attributionLookbackWindowDays | number | 7, 30 or 40 — Google publishes no other value. Defaults to 30 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"kind": {
"enum": [
"merchant_center_destination",
"google_analytics_link"
],
"type": "string",
"description": "for action:'create' — which kind to make. Passing propertyId implies the Google Analytics one"
},
"action": {
"enum": [
"list",
"create",
"update",
"delete",
"undelete"
],
"type": "string",
"description": "defaults to 'list'"
},
"confirm": {
"type": "boolean",
"description": "must be true to actually archive"
},
"propertyId": {
"type": "string",
"description": "NUMERIC GA4 property id for a Google Analytics link (from list_analytics_properties), not a G- measurement id"
},
"displayName": {
"type": "string",
"description": "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"
},
"showDeleted": {
"type": "boolean",
"description": "include archived sources in a list"
},
"currencyCode": {
"type": "string",
"description": "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"
},
"conversionTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "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"
},
"attributionModel": {
"type": "string",
"description": "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"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
},
"conversionSourceId": {
"type": "string",
"description": "for update, delete and undelete — from action:'list'"
},
"attributionLookbackWindowDays": {
"type": "number",
"description": "7, 30 or 40 — Google publishes no other value. Defaults to 30"
}
}
}| boolean |
| subscribe for every managed account instead of just this one — mutually exclusive with targetAccount |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"action": {
"enum": [
"list",
"create",
"delete"
],
"type": "string",
"description": "defaults to 'list'"
},
"callBackUri": {
"type": "string",
"description": "HTTPS endpoint Google will POST to (create). Not a Hermoso URL and not localhost"
},
"targetAccount": {
"type": "string",
"description": "accounts/{id} to receive notifications for; defaults to this account"
},
"subscriptionId": {
"type": "string",
"description": "for action:'delete', from action:'list'"
},
"registeredEvent": {
"enum": [
"PRODUCT_STATUS_CHANGE",
"ACCOUNT_SERVICE_CHANGE"
],
"type": "string",
"description": "defaults to PRODUCT_STATUS_CHANGE, the one that fires on a disapproval"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
},
"allManagedAccounts": {
"type": "boolean",
"description": "subscribe for every managed account instead of just this one — mutually exclusive with targetAccount"
}
}
}| confirm | boolean | true ONLY after the user approved the change the first call described |
| program | string | programs enable/disable |
| section* | string | shipping, returns, business_info, homepage or programs; the Merchant Center area to act on |
| services | array | shipping upsert/replace |
| overwrite | boolean | homepage claim: take the claim from another account |
| warehouses | array | shipping: Google Warehouse objects; omitted = existing ones kept |
| serviceNames | array | shipping remove |
| returnPolicyId | string | returns delete, from list |
| customerService | object | business_info: uri, email, phone (E.164) |
| merchantCenterId* | string | from list_merchant_accounts |
| koreanBusinessRegistrationNumber | string | Korean business registration number; business_info update only |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"section"
],
"properties": {
"uri": {
"type": "string",
"description": "homepage set_uri"
},
"action": {
"enum": [
"get",
"upsert",
"remove",
"replace",
"list",
"create",
"delete",
"update",
"claim",
"unclaim",
"set_uri",
"enable",
"disable"
],
"type": "string",
"description": "defaults to the section's read (get/list)"
},
"policy": {
"type": "object",
"description": "returns create",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"address": {
"type": "object",
"description": "business_info: regionCode, street or addressLines, locality, administrativeArea, postalCode",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"confirm": {
"type": "boolean",
"description": "true ONLY after the user approved the change the first call described"
},
"program": {
"type": "string",
"description": "programs enable/disable"
},
"section": {
"enum": [
"shipping",
"returns",
"business_info",
"homepage",
"programs"
],
"type": "string",
"description": "shipping, returns, business_info, homepage or programs; the Merchant Center area to act on"
},
"services": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "shipping upsert/replace"
},
"overwrite": {
"type": "boolean",
"description": "homepage claim: take the claim from another account"
},
"warehouses": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "shipping: Google Warehouse objects; omitted = existing ones kept"
},
"serviceNames": {
"type": "array",
"items": {
"type": "string"
},
"description": "shipping remove"
},
"returnPolicyId": {
"type": "string",
"description": "returns delete, from list"
},
"customerService": {
"type": "object",
"description": "business_info: uri, email, phone (E.164)",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
},
"koreanBusinessRegistrationNumber": {
"type": "string",
"description": "Korean business registration number; business_info update only"
}
}
}| campaignIds |
| array |
| campaigns to attach to or detach from the list (action attach or detach) |
| negativeKeywordIds | array | keyword ids to remove from the list (action remove) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"action"
],
"properties": {
"name": {
"type": "string",
"description": "create: ≤255 characters"
},
"action": {
"enum": [
"create",
"add",
"remove",
"attach",
"detach",
"delete"
],
"type": "string",
"description": "create, add, remove, attach, detach or delete a shared negative keyword list"
},
"listId": {
"type": "string",
"description": "every action but create"
},
"confirm": {
"type": "boolean",
"description": "true to delete the list, or to remove, detach or delete on a LIVE campaign"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"description": "≤100 characters"
},
"matchType": {
"enum": [
"Exact",
"Phrase"
],
"type": "string",
"description": "default Phrase — Microsoft has no broad negative"
}
}
},
"description": "keywords for create or add: [{text, matchType}]"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "campaigns to attach to or detach from the list (action attach or detach)"
},
"negativeKeywordIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "keyword ids to remove from the list (action remove)"
}
}
}| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| isPublishingEnabled | boolean | products serve ONLY when this is true — never defaulted on |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "unique within the store, maximum 70 characters"
},
"market": {
"type": "string",
"description": "REQUIRED when creating — where the products are served, e.g. en-US. Not settable afterwards"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to delete"
},
"deleteIt": {
"type": "boolean",
"description": "true to delete the catalog named by catalogId"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"catalogId": {
"type": "string",
"description": "set to UPDATE or DELETE an existing catalog; omit to create a new one"
},
"merchantId": {
"type": "string",
"description": "Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"isPublishingEnabled": {
"type": "boolean",
"description": "products serve ONLY when this is true — never defaulted on"
}
}
}| Unix seconds |
| adAccountId | string | Pinterest ad account id — omit to use the single shared account |
| promotionId | string | update, delete: from pinterest_ads_account_data what:promotions |
| discountStatus | string | Pinterest’s discount_status value |
| templateValues | array | what fills the offer text, at most 2, in Pinterest template_values shape |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"code": {
"type": "string",
"description": "the code a buyer redeems"
},
"type": {
"enum": [
"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"
],
"type": "string",
"description": "create: the kind of offer"
},
"title": {
"type": "string",
"description": "create: the internal name"
},
"action": {
"enum": [
"create",
"update",
"delete"
],
"type": "string",
"description": "default create"
},
"fields": {
"type": "object",
"description": "any other promotion field, by Pinterest’s own name",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"confirm": {
"type": "boolean",
"description": "delete: required true"
},
"endTime": {
"type": "number",
"description": "Unix seconds"
},
"customId": {
"type": "string",
"description": "your own id for it"
},
"startTime": {
"type": "number",
"description": "Unix seconds"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"promotionId": {
"type": "string",
"description": "update, delete: from pinterest_ads_account_data what:promotions"
},
"discountStatus": {
"type": "string",
"description": "Pinterest’s discount_status value"
},
"templateValues": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "what fills the offer text, at most 2, in Pinterest template_values shape"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "create: the template name"
},
"action": {
"enum": [
"create",
"remove"
],
"type": "string",
"description": "default create"
},
"keywords": {
"type": "array",
"items": {
"type": "object",
"required": [
"value"
],
"properties": {
"value": {
"type": "string",
"description": "the search words, up to 120 characters"
},
"matchType": {
"enum": [
"BROAD",
"PHRASE",
"EXACT",
"EXACT_NEGATIVE",
"PHRASE_NEGATIVE"
],
"type": "string",
"description": "default BROAD; a campaign takes only the _NEGATIVE types"
}
}
},
"description": "keywords to hold"
},
"templateId": {
"type": "string",
"description": "remove: from pinterest_ads_account_data what:targeting_templates"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"targetingSpec": {
"type": "object",
"description": "Pinterest targeting object, e.g. {\"GEO\":[\"US\"],\"AGE_BUCKET\":[\"25-34\"]}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"placementGroup": {
"enum": [
"ALL",
"SEARCH",
"BROWSE",
"OTHER"
],
"type": "string",
"description": "create: default ALL"
},
"autoTargetingEnabled": {
"type": "boolean",
"description": "create: default true at Pinterest"
}
}
}| the filled-cell count an unconfirmed delete reported |
| spreadsheetId | string | Google spreadsheet id; use this or sheetUrl |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"action"
],
"properties": {
"tab": {
"type": "string",
"description": "which tab — its title or numeric sheetId (rename / delete)"
},
"title": {
"type": "string",
"description": "the name for the new tab (action:\"add\")"
},
"action": {
"enum": [
"add",
"rename",
"delete"
],
"type": "string",
"description": "add, rename or delete a tab"
},
"confirm": {
"type": "boolean",
"description": "true deletes a tab that holds data; needs confirmCells"
},
"newTitle": {
"type": "string",
"description": "what to rename the tab to (action:\"rename\")"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"confirmCells": {
"type": "number",
"description": "the filled-cell count an unconfirmed delete reported"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
}
}
}| for delete, or test of one integration |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"action": {
"enum": [
"subscribe",
"list",
"test",
"delete"
],
"type": "string",
"description": "default subscribe"
},
"formId": {
"type": "string",
"description": "the lead form (list_snapchat_ads_lead_forms)"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true for delete"
},
"forwardTo": {
"type": "string",
"description": "optional public https URL each lead is also forwarded to"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"integrationId": {
"type": "string",
"description": "for delete, or test of one integration"
}
}
}| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"operation"
],
"properties": {
"words": {
"type": "array",
"items": {
"type": "string"
},
"description": "for add and remove"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true for remove — without it nothing is removed and you get the list of which words are actually blocked plus what unhiding them will do"
},
"newWord": {
"type": "string",
"description": "for replace"
},
"oldWord": {
"type": "string",
"description": "for replace"
},
"operation": {
"enum": [
"add",
"remove",
"replace"
],
"type": "string",
"description": "add (append words), remove (needs confirm to unhide), or replace (oldWord swapped for newWord)"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}| string |
| required for delete; filters a list |
| minute | number | 0-59 |
| confirm | boolean | REQUIRED true for action:"delete" |
| feedName | string | required for create |
| password | string | password for a protected feed URL; pair with username |
| timezone | string | IANA zone, e.g. America/New_York — required alongside uri and intervalType |
| username | string | only if the feed URL is password protected |
| catalogId* | string | from list_tiktok_ads_catalogs |
| dayOfMonth | number | day of the month (1-31) a MONTHLY feed import runs; used with intervalType MONTHLY |
| updateMode | string | REQUIRED on create and deliberately without a default — OVERWRITE REMOVES products missing from the feed |
| intervalType | string | required alongside uri and timezone |
| intervalCount | number | HOURLY 1-23, DAILY 1-30, MONTHLY 1-12 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId"
],
"properties": {
"uri": {
"type": "string",
"description": "http/https/ftp/sftp URL of the feed file — CSV, TSV or XML (RSS/ATOM), up to 8GB"
},
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers"
},
"hour": {
"type": "number",
"description": "0-23"
},
"action": {
"enum": [
"list",
"create",
"delete"
],
"type": "string",
"description": "default list"
},
"feedId": {
"type": "string",
"description": "required for delete; filters a list"
},
"minute": {
"type": "number",
"description": "0-59"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true for action:\"delete\""
},
"feedName": {
"type": "string",
"description": "required for create"
},
"password": {
"type": "string",
"description": "password for a protected feed URL; pair with username"
},
"timezone": {
"type": "string",
"description": "IANA zone, e.g. America/New_York — required alongside uri and intervalType"
},
"username": {
"type": "string",
"description": "only if the feed URL is password protected"
},
"catalogId": {
"type": "string",
"description": "from list_tiktok_ads_catalogs"
},
"dayOfMonth": {
"type": "number",
"description": "day of the month (1-31) a MONTHLY feed import runs; used with intervalType MONTHLY"
},
"updateMode": {
"enum": [
"OVERWRITE",
"INCREMENTAL"
],
"type": "string",
"description": "REQUIRED on create and deliberately without a default — OVERWRITE REMOVES products missing from the feed"
},
"intervalType": {
"enum": [
"HOURLY",
"DAILY",
"MONTHLY"
],
"type": "string",
"description": "required alongside uri and timezone"
},
"intervalCount": {
"type": "number",
"description": "HOURLY 1-23, DAILY 1-30, MONTHLY 1-12"
}
}
}| advertiserId |
| string |
| TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| autoTracking | boolean | attach EVERY future campaign on this advertiser to this event set. Max ten per advertiser. Off unless you mean it. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"action"
],
"properties": {
"name": {
"type": "string",
"description": "required for create; max 40 characters, and TikTok refuses a name already used on the account"
},
"action": {
"enum": [
"create",
"update",
"delete"
],
"type": "string",
"description": "create, update or delete (delete needs confirm)"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to delete"
},
"eventSetId": {
"type": "string",
"description": "required for update and delete — from list_tiktok_ads_offline_event_sets"
},
"description": {
"type": "string",
"description": "free-text description of the event set; optional"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"autoTracking": {
"type": "boolean",
"description": "attach EVERY future campaign on this advertiser to this event set. Max ten per advertiser. Off unless you mean it."
}
}
}| anchorTitle |
| string |
| required for create: the call-to-action wording |
| countryCode | string | required for create: the anchor is invisible outside it |
| thumbnailUrl | string | required for create: public URL, exactly 210x375, under 2MB |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
| landingPageUrl | string | required for create: a product or service DETAIL page |
| categoryLabelId | string | required for create: TikTok’s product/service category, and it decides which countries the anchor may use |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"action": {
"enum": [
"list",
"create",
"delete"
],
"type": "string",
"description": "default list"
},
"anchorId": {
"type": "string",
"description": "required for delete"
},
"pageSize": {
"type": "number",
"description": "1-50"
},
"anchorIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "filters a list, max 100"
},
"anchorName": {
"type": "string",
"description": "required for create: an internal label TikTok never shows viewers, ≤32 characters"
},
"anchorTitle": {
"enum": [
"WATCH_NOW",
"LISTEN_NOW",
"READ_MORE",
"SHOW_NOW",
"GET_OFFER",
"LEARN_MORE",
"CONTACT_US",
"JOIN_NOW",
"APPLY_NOW"
],
"type": "string",
"description": "required for create: the call-to-action wording"
},
"countryCode": {
"type": "string",
"description": "required for create: the anchor is invisible outside it"
},
"thumbnailUrl": {
"type": "string",
"description": "required for create: public URL, exactly 210x375, under 2MB"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
},
"landingPageUrl": {
"type": "string",
"description": "required for create: a product or service DETAIL page"
},
"categoryLabelId": {
"type": "string",
"description": "required for create: TikTok’s product/service category, and it decides which countries the anchor may use"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId"
],
"properties": {
"offerId": {
"type": "string",
"description": "for one PRODUCT's issues; omit for the account's issues"
},
"timeZone": {
"type": "string",
"description": "IANA zone for times inside the content, e.g. 'America/Los_Angeles'"
},
"feedLabel": {
"type": "string",
"description": "REQUIRED alongside offerId — from list_merchant_products. A product is addressed by contentLanguage + feedLabel + offerId"
},
"languageCode": {
"type": "string",
"description": "BCP-47 language for the fix text, e.g. 'en-US'"
},
"contentLanguage": {
"type": "string",
"description": "with offerId; defaults to 'en'"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}| pageToken | string | nextPageToken from a previous call — resend the IDENTICAL query and pageSize with it, which Google requires |
| merchantCenterId* | string | from list_merchant_accounts — a STANDALONE account or a SUBACCOUNT, never a multi-client (MCA) account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"query"
],
"properties": {
"query": {
"type": "string",
"description": "the MCQL query, e.g. \"SELECT id, title, price, suggested_price, effectiveness FROM price_insights_product_view\""
},
"pageSize": {
"type": "number",
"description": "rows per response, 1–100000 (default 1000). 100,000 is GOOGLE's ceiling, not ours."
},
"pageToken": {
"type": "string",
"description": "nextPageToken from a previous call — resend the IDENTICAL query and pageSize with it, which Google requires"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts — a STANDALONE account or a SUBACCOUNT, never a multi-client (MCA) account"
}
}
}| until | string | end date YYYY-MM-DD |
| objectId | string | a campaign / ad set / ad id to scope to (default: the whole account) |
| breakdowns | string | comma-separated, e.g. "age,gender" | "publisher_platform,platform_position" | "country" | "impression_device" |
| datePreset | string | Meta's date_preset enum, default last_30d: today | yesterday | last_3d | last_7d | last_14d | last_28d | last_30d | last_90d | this_week_mon_today | this_week_sun_today | last_week_mon_sun | last_week_sun_sat | this_month | last_month | this_quarter | last_quarter | this_year | last_year | maximum | data_maximum. NOT "lifetime" — Meta retired it in v10.0; use "maximum" (37 months) or since+until. |
| adAccountId* | string | ad account id (act_… or digits) |
| actionBreakdowns | string | comma-separated, e.g. "action_type,action_device" — splits the conversion/action counts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adAccountId"
],
"properties": {
"level": {
"enum": [
"account",
"campaign",
"adset",
"ad"
],
"type": "string",
"description": "break the numbers down by this level"
},
"since": {
"type": "string",
"description": "start date YYYY-MM-DD (use with until). Meta keeps 37 months: an older start is moved to the oldest day Meta has, and the note says so"
},
"until": {
"type": "string",
"description": "end date YYYY-MM-DD"
},
"objectId": {
"type": "string",
"description": "a campaign / ad set / ad id to scope to (default: the whole account)"
},
"breakdowns": {
"type": "string",
"description": "comma-separated, e.g. \"age,gender\" | \"publisher_platform,platform_position\" | \"country\" | \"impression_device\""
},
"datePreset": {
"type": "string",
"description": "Meta's date_preset enum, default last_30d: today | yesterday | last_3d | last_7d | last_14d | last_28d | last_30d | last_90d | this_week_mon_today | this_week_sun_today | last_week_mon_sun | last_week_sun_sat | this_month | last_month | this_quarter | last_quarter | this_year | last_year | maximum | data_maximum. NOT \"lifetime\" — Meta retired it in v10.0; use \"maximum\" (37 months) or since+until."
},
"adAccountId": {
"type": "string",
"description": "ad account id (act_… or digits)"
},
"actionBreakdowns": {
"type": "string",
"description": "comma-separated, e.g. \"action_type,action_device\" — splits the conversion/action counts"
}
}
}| string |
| default Account |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"since": {
"type": "string",
"description": "YYYY-MM-DD, default 30 days back"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD, default today"
},
"segments": {
"type": "array",
"items": {
"enum": [
"Day",
"DayOfWeek",
"Device"
],
"type": "string"
},
"description": "at most three — Microsoft’s own limit"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"entityIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED for Campaign / AdGroup / Keyword, max 200. Microsoft only defaults to the whole account for entityType Account"
},
"entityType": {
"enum": [
"Account",
"Campaign",
"AdGroup",
"Keyword"
],
"type": "string",
"description": "default Account"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}| array |
| Microsoft DownloadEntity names — Campaigns, AdGroups, Ads, Keywords (the default), plus SitelinkAdExtensions, CalloutAdExtensions, Labels, Budgets, BidStrategies, AssetGroups, ConversionGoal, Experiments, Audiences and ~180 more. An unknown one is refused BY NAME, because Microsoft answers it with a deserialization error that names no field and reads like an outage |
| fileType | string | default Csv |
| accountId | string | older name for adAccountId (MCP only) |
| dataScope | array | EntityData (default), QualityScoreData, BidSuggestionsData — the last two force a FULL export |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| campaignIds | array | export only these campaigns (max 1000) instead of the whole account |
| compression | string | default GZip |
| downloadRequestId | string | pick up an export that came back pending. Valid 2 days, and far cheaper than exporting the account again |
| allowUnlistedEntities | boolean | send an entity name we do not recognise verbatim — for a record type Microsoft added after our copy of its list was read |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "how many rows to return inline, default 200 (the full file is still in `file`)"
},
"since": {
"type": "string",
"description": "ISO date — export only what CHANGED since then. Microsoft refuses a delta older than 30 days, and refuses one at all alongside QualityScoreData / BidSuggestionsData"
},
"waitMs": {
"type": "number",
"description": "how long to wait for Microsoft to build the file before returning pending, max 120000"
},
"entities": {
"type": "array",
"items": {
"type": "string"
},
"description": "Microsoft DownloadEntity names — Campaigns, AdGroups, Ads, Keywords (the default), plus SitelinkAdExtensions, CalloutAdExtensions, Labels, Budgets, BidStrategies, AssetGroups, ConversionGoal, Experiments, Audiences and ~180 more. An unknown one is refused BY NAME, because Microsoft answers it with a deserialization error that names no field and reads like an outage"
},
"fileType": {
"enum": [
"Csv",
"Tsv"
],
"type": "string",
"description": "default Csv"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"dataScope": {
"type": "array",
"items": {
"type": "string"
},
"description": "EntityData (default), QualityScoreData, BidSuggestionsData — the last two force a FULL export"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "export only these campaigns (max 1000) instead of the whole account"
},
"compression": {
"enum": [
"GZip",
"Zip"
],
"type": "string",
"description": "default GZip"
},
"downloadRequestId": {
"type": "string",
"description": "pick up an export that came back pending. Valid 2 days, and far cheaper than exporting the account again"
},
"allowUnlistedEntities": {
"type": "boolean",
"description": "send an entity name we do not recognise verbatim — for a record type Microsoft added after our copy of its list was read"
}
}
}| number |
| how long to wait for Microsoft to apply the file before returning pending, max 120000 |
| confirm | boolean | the user has SEEN what this file activates and deletes and said yes. Not sufficient on its own — the two echo counts below are also required |
| fileType | string | inferred from the file when omitted |
| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| pauseInstead | boolean | rewrite every activation to Paused so the rest of the file uploads with nothing armed. Never applied unless asked: an unrequested rewrite would PAUSE A LIVE CAMPAIGN |
| responseMode | string | default ErrorsAndResults, so the outcome can be read back rather than assumed |
| uploadRequestId | string | read the outcome of an upload that came back pending. NEVER re-upload the file — it is already queued, and a second upload applies every row twice |
| confirmDeletions | number | echo back how many objects this file DELETES — the exact count the un-confirmed call reported |
| confirmActivations | number | echo back how many objects this file ACTIVATES — the exact count the un-confirmed call reported |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"file"
],
"properties": {
"file": {
"type": "string",
"description": "the bulk file itself, CSV or TSV, whose first column header must be \"Type\""
},
"dryRun": {
"type": "boolean",
"description": "report what the file would do and upload nothing at all"
},
"waitMs": {
"type": "number",
"description": "how long to wait for Microsoft to apply the file before returning pending, max 120000"
},
"confirm": {
"type": "boolean",
"description": "the user has SEEN what this file activates and deletes and said yes. Not sufficient on its own — the two echo counts below are also required"
},
"fileType": {
"enum": [
"Csv",
"Tsv"
],
"type": "string",
"description": "inferred from the file when omitted"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"pauseInstead": {
"type": "boolean",
"description": "rewrite every activation to Paused so the rest of the file uploads with nothing armed. Never applied unless asked: an unrequested rewrite would PAUSE A LIVE CAMPAIGN"
},
"responseMode": {
"enum": [
"ErrorsAndResults",
"ErrorsOnly"
],
"type": "string",
"description": "default ErrorsAndResults, so the outcome can be read back rather than assumed"
},
"uploadRequestId": {
"type": "string",
"description": "read the outcome of an upload that came back pending. NEVER re-upload the file — it is already queued, and a second upload applies every row twice"
},
"confirmDeletions": {
"type": "number",
"description": "echo back how many objects this file DELETES — the exact count the un-confirmed call reported"
},
"confirmActivations": {
"type": "number",
"description": "echo back how many objects this file ACTIVATES — the exact count the un-confirmed call reported"
}
}
}| categoryId | number | Microsoft keyword category id to mine ideas from |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| competition | array | filter to Low | Medium | High |
| expandIdeas | boolean | false = do not expand; then keywords[] is mandatory |
| locationIds* | array | REQUIRED — Microsoft location ids (microsoft_ads_geo_search resolves names to ids, free) |
| maxSearchVolume | number | only ideas with at most this many monthly searches |
| minSearchVolume | number | only ideas with at least this many monthly searches |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"locationIds"
],
"properties": {
"url": {
"type": "string",
"description": "a landing page for Microsoft to mine ideas from"
},
"network": {
"enum": [
"OwnedAndOperatedAndSyndicatedSearch",
"OwnedAndOperatedOnly",
"SyndicatedSearchOnly"
],
"type": "string",
"description": "search network the volume is measured on: Microsoft-owned, partner-syndicated or both"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "seed terms to expand from"
},
"language": {
"type": "string",
"description": "default English"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"attributes": {
"type": "array",
"items": {
"type": "string"
},
"description": "which idea attributes to return — omit for all"
},
"categoryId": {
"type": "number",
"description": "Microsoft keyword category id to mine ideas from"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"competition": {
"type": "array",
"items": {
"type": "string"
},
"description": "filter to Low | Medium | High"
},
"expandIdeas": {
"type": "boolean",
"description": "false = do not expand; then keywords[] is mandatory"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED — Microsoft location ids (microsoft_ads_geo_search resolves names to ids, free)"
},
"maxSearchVolume": {
"type": "number",
"description": "only ideas with at most this many monthly searches"
},
"minSearchVolume": {
"type": "number",
"description": "only ideas with at least this many monthly searches"
}
}
}| also show recommendations already dismissed |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "Microsoft’s MaxCount, omit for everything"
},
"types": {
"type": "array",
"items": {
"type": "string"
},
"description": "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"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"includeDismissed": {
"type": "boolean",
"description": "also show recommendations already dismissed"
}
}
}| YYYY-MM-DD custom range end |
| columns | array | report columns — defaults to campaign performance. Each report type accepts only its OWN column set; Microsoft also refuses impression-share columns alongside BidMatchType / BudgetName / DeviceOS / Goal / TopVsOther in the same request. |
| accountId | string | older name for adAccountId (MCP only) |
| reportType | string | which report — default CampaignPerformanceReportRequest. An unknown name is refused with the full list. |
| timePeriod | string | predefined Microsoft window, default Last30Days — must be one of the values in the description; anything else is rejected |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| aggregation | string | default Summary. Hourly accepts only Today/Yesterday or a custom range. |
| reportRequestId | string | pick up a report that came back pending — pass it back and this RESUMES that exact report instead of submitting a new one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"since": {
"type": "string",
"description": "YYYY-MM-DD custom range start (with until)"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD custom range end"
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "report columns — defaults to campaign performance. Each report type accepts only its OWN column set; Microsoft also refuses impression-share columns alongside BidMatchType / BudgetName / DeviceOS / Goal / TopVsOther in the same request."
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"reportType": {
"type": "string",
"description": "which report — default CampaignPerformanceReportRequest. An unknown name is refused with the full list."
},
"timePeriod": {
"type": "string",
"description": "predefined Microsoft window, default Last30Days — must be one of the values in the description; anything else is rejected"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"aggregation": {
"enum": [
"Summary",
"Hourly",
"Daily",
"Weekly",
"Monthly",
"Yearly",
"HourOfDay",
"DayOfWeek",
"WeeklyStartingMonday"
],
"type": "string",
"description": "default Summary. Hourly accepts only Today/Yesterday or a custom range."
},
"reportRequestId": {
"type": "string",
"description": "pick up a report that came back pending — pass it back and this RESUMES that exact report instead of submitting a new one"
}
}
}| Microsoft ad account id — omit to use the single shared account |
| dailyBudget | number | daily budget in account currency the estimate assumes |
| locationIds* | array | REQUIRED — Microsoft location ids |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"keywords",
"maxCpc",
"locationIds"
],
"properties": {
"maxCpc": {
"type": "number",
"description": "REQUIRED — the max CPC bid to estimate at, in the account currency"
},
"network": {
"type": "string",
"description": "search network to estimate for; same values as microsoft_ads_keyword_ideas network"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "the keywords to estimate"
},
"language": {
"type": "string",
"description": "language to estimate for, e.g. English"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"matchType": {
"enum": [
"Exact",
"Phrase",
"Broad"
],
"type": "string",
"description": "default Exact"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"dailyBudget": {
"type": "number",
"description": "daily budget in account currency the estimate assumes"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED — Microsoft location ids"
}
}
}| length |
| number |
| the conversion window, counted in lengthUnits — the TOTAL may not exceed 90 days. Omit to use the funnel own saved value |
| toDate | string | YYYY-MM-DD (default today) |
| fromDate | string | YYYY-MM-DD (default 30 days ago) |
| funnelId | string | omit to list the saved funnels and their ids |
| interval | number | the number of days each bucket covers — an alternative to "unit" (default 1) |
| lengthUnit | string | the unit "length" is counted in (Mixpanel offers no week or month here). Omit to use the funnel own saved value |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"on": {
"type": "string",
"description": "a property expression to break the funnel down by"
},
"unit": {
"enum": [
"day",
"week",
"month"
],
"type": "string",
"description": "the bucket the results are grouped into"
},
"limit": {
"type": "number",
"description": "top N property values; Mixpanel defaults to 255, max 10000, and it does nothing unless \"on\" is set"
},
"where": {
"type": "string",
"description": "a segmentation expression to filter by"
},
"length": {
"type": "number",
"description": "the conversion window, counted in lengthUnits — the TOTAL may not exceed 90 days. Omit to use the funnel own saved value"
},
"toDate": {
"type": "string",
"description": "YYYY-MM-DD (default today)"
},
"fromDate": {
"type": "string",
"description": "YYYY-MM-DD (default 30 days ago)"
},
"funnelId": {
"type": "string",
"description": "omit to list the saved funnels and their ids"
},
"interval": {
"type": "number",
"description": "the number of days each bucket covers — an alternative to \"unit\" (default 1)"
},
"lengthUnit": {
"enum": [
"second",
"minute",
"hour",
"day"
],
"type": "string",
"description": "the unit \"length\" is counted in (Mixpanel offers no week or month here). Omit to use the funnel own saved value"
}
}
}| dataGroupId |
| string |
| group key — for GROUP profiles rather than user profiles |
| distinctIds | array | several people by distinct id |
| includeAllUsers | boolean | only applies alongside cohortId; false = only distinct ids that actually have a profile |
| outputProperties | array | only return these properties — strongly recommended, it is much faster and much smaller |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "pages start at ZERO; any page above 0 REQUIRES sessionId"
},
"where": {
"type": "string",
"description": "a Mixpanel segmentation expression over profile properties, e.g. properties[\"$city\"] == \"London\""
},
"cohortId": {
"type": "string",
"description": "a numeric cohort id from list_mixpanel_cohorts"
},
"sessionId": {
"type": "string",
"description": "from the previous page — required for page > 0"
},
"distinctId": {
"type": "string",
"description": "one person"
},
"dataGroupId": {
"type": "string",
"description": "group key — for GROUP profiles rather than user profiles"
},
"distinctIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "several people by distinct id"
},
"includeAllUsers": {
"type": "boolean",
"description": "only applies alongside cohortId; false = only distinct ids that actually have a profile"
},
"outputProperties": {
"type": "array",
"items": {
"type": "string"
},
"description": "only return these properties — strongly recommended, it is much faster and much smaller"
}
}
}| where | string | a Mixpanel segmentation expression filtering the RETURNING event |
| toDate | string | YYYY-MM-DD (default today) |
| fromDate | string | YYYY-MM-DD (default 30 days ago) |
| 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) |
| bornEvent | string | the event that puts someone into the cohort — REQUIRED by Mixpanel when retentionType is "birth", which is the default |
| bornWhere | string | a Mixpanel segmentation expression filtering who ENTERS the cohort (the born event) |
| intervalCount | number | HOW MANY buckets to return (default 1). A "0th" bucket is always included for events inside the first interval |
| retentionType | string | "birth" = cohort by first occurrence of bornEvent; "compounded" = anyone active |
| unboundedRetention | boolean | accumulate right-to-left, so day N means "retained on day N or any day after" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"on": {
"type": "string",
"description": "a property expression to segment the returning event on — this is what breaks a retention curve down by e.g. plan or platform"
},
"unit": {
"enum": [
"day",
"week",
"month"
],
"type": "string",
"description": "the interval unit (default day)"
},
"event": {
"type": "string",
"description": "the returning event; omitted means any event"
},
"limit": {
"type": "number",
"description": "top N segmentation values; does nothing unless \"on\" is set"
},
"where": {
"type": "string",
"description": "a Mixpanel segmentation expression filtering the RETURNING event"
},
"toDate": {
"type": "string",
"description": "YYYY-MM-DD (default today)"
},
"fromDate": {
"type": "string",
"description": "YYYY-MM-DD (default 30 days ago)"
},
"interval": {
"type": "number",
"description": "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)"
},
"bornEvent": {
"type": "string",
"description": "the event that puts someone into the cohort — REQUIRED by Mixpanel when retentionType is \"birth\", which is the default"
},
"bornWhere": {
"type": "string",
"description": "a Mixpanel segmentation expression filtering who ENTERS the cohort (the born event)"
},
"intervalCount": {
"type": "number",
"description": "HOW MANY buckets to return (default 1). A \"0th\" bucket is always included for events inside the first interval"
},
"retentionType": {
"enum": [
"birth",
"compounded"
],
"type": "string",
"description": "\"birth\" = cohort by first occurrence of bornEvent; \"compounded\" = anyone active"
},
"unboundedRetention": {
"type": "boolean",
"description": "accumulate right-to-left, so day N means \"retained on day N or any day after\""
}
}
}| toDate | string | YYYY-MM-DD (default today) |
| fromDate | string | YYYY-MM-DD (default 30 days ago) |
| interval | number | the number of days each bucket covers — Mixpanel offers this in lieu of "unit" when "type" is not "general" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"event"
],
"properties": {
"on": {
"type": "string",
"description": "a segmentation expression to break down by, e.g. properties[\"$browser\"]"
},
"type": {
"enum": [
"general",
"unique",
"average"
],
"type": "string",
"description": "count type: general (event count, default), unique (distinct users) or average"
},
"unit": {
"enum": [
"minute",
"hour",
"day",
"month"
],
"type": "string",
"description": "NOTE: Mixpanel offers no \"week\" on segmentation, unlike retention"
},
"event": {
"type": "string",
"description": "a SINGLE event name — not an array"
},
"limit": {
"type": "number",
"description": "top N property values; Mixpanel defaults to 60, max 10000, and it does nothing unless \"on\" is set"
},
"where": {
"type": "string",
"description": "a segmentation expression to filter by"
},
"toDate": {
"type": "string",
"description": "YYYY-MM-DD (default today)"
},
"fromDate": {
"type": "string",
"description": "YYYY-MM-DD (default 30 days ago)"
},
"interval": {
"type": "number",
"description": "the number of days each bucket covers — Mixpanel offers this in lieu of \"unit\" when \"type\" is not \"general\""
}
}
}| array |
| 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 |
| 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. |
| 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. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"operations"
],
"properties": {
"operations": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "1–1000 operations: {operation_id, type, idempotency_key (creates) or target_resource_id (updates), input:{…}}"
},
"validateOnly": {
"type": "boolean",
"description": "true = FREE dry run; nothing is created or changed"
},
"idempotencyKey": {
"type": "string",
"description": "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."
},
"partialFailure": {
"type": "boolean",
"description": "default true (independent operations continue after an error). false skips later operations after a failure and does NOT roll back what already completed."
}
}
}| level |
| string |
| inferred from which id you pass — default ad_account |
| limit | number | with recentEvents: how many events, 1-50 (default 50) |
| since | string | YYYY-MM-DD |
| until | string | YYYY-MM-DD |
| pixelId | string | with recentEvents: the pixel_id to read; omit to use the account's first pixel |
| adGroupId | string | ad group id to report on; sets the level to ad_group |
| breakdown | string | split each row by device or by country |
| entityIds | array | 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 |
| eventNames | array | only these events, e.g. ["order_created"] |
| granularity | string | daily = one row per day per entity, plus a byDate total; default none = one total per entity |
| recentEvents | boolean | true = list a recent sample of the events OpenAI RECEIVED on the pixel instead of attributed totals |
| viewWindowDays | any | default 1; 0 = no views |
| clickWindowDays | any | default 30 |
| includeZeroRows | boolean | default true |
| attributedEvents | boolean | default true; false = goal counts only |
| attributionTimeBasis | string | default ad_event_time |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad id to report on; sets the level to ad"
},
"level": {
"enum": [
"ad_account",
"campaign",
"ad_group",
"ad"
],
"type": "string",
"description": "inferred from which id you pass — default ad_account"
},
"limit": {
"type": "number",
"description": "with recentEvents: how many events, 1-50 (default 50)"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD"
},
"pixelId": {
"type": "string",
"description": "with recentEvents: the pixel_id to read; omit to use the account's first pixel"
},
"adGroupId": {
"type": "string",
"description": "ad group id to report on; sets the level to ad_group"
},
"breakdown": {
"enum": [
"device",
"country"
],
"type": "string",
"description": "split each row by device or by country"
},
"entityIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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": {
"type": "string",
"description": "campaign id to report on; sets the level to campaign"
},
"eventNames": {
"type": "array",
"items": {
"type": "string"
},
"description": "only these events, e.g. [\"order_created\"]"
},
"granularity": {
"enum": [
"none",
"daily"
],
"type": "string",
"description": "daily = one row per day per entity, plus a byDate total; default none = one total per entity"
},
"recentEvents": {
"type": "boolean",
"description": "true = list a recent sample of the events OpenAI RECEIVED on the pixel instead of attributed totals"
},
"viewWindowDays": {
"anyOf": [
{
"type": "number",
"const": 0
},
{
"type": "number",
"const": 1
}
],
"description": "default 1; 0 = no views"
},
"clickWindowDays": {
"anyOf": [
{
"type": "number",
"const": 7
},
{
"type": "number",
"const": 14
},
{
"type": "number",
"const": 30
}
],
"description": "default 30"
},
"includeZeroRows": {
"type": "boolean",
"description": "default true"
},
"attributedEvents": {
"type": "boolean",
"description": "default true; false = goal counts only"
},
"attributionTimeBasis": {
"enum": [
"ad_event_time",
"conversion_time"
],
"type": "string",
"description": "default ad_event_time"
}
}
}| array |
| filters on hotel attributes or metrics |
| breakdown | string | expand each property into hierarchy rows; each row repeats its property total under `property` |
| sortField | string | metric to sort rows by |
| adGroupIds | array | return only these ad group ids |
| properties | array | exact feed/property pairs |
| timeseries | boolean | true = daily totals across the selected properties |
| campaignIds | array | return only these campaign ids |
| granularity | string | none (one total per property), daily or monthly rows |
| sortDirection | string | asc or desc |
| includeTotalCount | boolean | true to return the total row count |
| includeZeroActivity | boolean | also list active properties with no delivery |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"since",
"until"
],
"properties": {
"adIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "return only these ad ids"
},
"after": {
"type": "string",
"description": "pagination cursor for the next page"
},
"limit": {
"type": "number",
"description": "property/time-bucket groups per page, up to 2000"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD, account timezone"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD, account timezone, no later than today"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"required": [
"field",
"operator",
"value"
],
"properties": {
"field": {
"type": "string",
"description": "hotel attribute (e.g. hotel_city) or metric (impressions, clicks, spend, ctr, cpc, cpm)"
},
"value": {
"description": "value to match: a list for IN, a number for a metric"
},
"operator": {
"enum": [
"IN",
"GREATER_THAN",
"LESS_THAN"
],
"type": "string",
"description": "IN for attributes; GREATER_THAN or LESS_THAN for metrics"
}
}
},
"description": "filters on hotel attributes or metrics"
},
"breakdown": {
"enum": [
"campaign",
"ad_group",
"ad"
],
"type": "string",
"description": "expand each property into hierarchy rows; each row repeats its property total under `property`"
},
"sortField": {
"enum": [
"impressions",
"clicks",
"spend",
"ctr",
"cpc",
"cpm"
],
"type": "string",
"description": "metric to sort rows by"
},
"adGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "return only these ad group ids"
},
"properties": {
"type": "array",
"items": {
"type": "object",
"required": [
"feedId",
"hotelId"
],
"properties": {
"feedId": {
"type": "string",
"description": "hotel feed id that holds the property"
},
"hotelId": {
"type": "string",
"description": "hotel id within that feed"
}
}
},
"description": "exact feed/property pairs"
},
"timeseries": {
"type": "boolean",
"description": "true = daily totals across the selected properties"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "return only these campaign ids"
},
"granularity": {
"enum": [
"none",
"daily",
"monthly"
],
"type": "string",
"description": "none (one total per property), daily or monthly rows"
},
"sortDirection": {
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "asc or desc"
},
"includeTotalCount": {
"type": "boolean",
"description": "true to return the total row count"
},
"includeZeroActivity": {
"type": "boolean",
"description": "also list active properties with no delivery"
}
}
}| Argument |
|---|
| Type |
|---|
| Description |
|---|
| adId | string | scope the report to one ad |
| sort | array | order rows, e.g. {field:"campaign.spend",direction:"desc"} |
| after | string | nextAfter from the previous page; limit goes up to 2000 rows |
| level | string | roll rows up to this level |
| limit | number | rows per page, up to 2000 |
| since | string | YYYY-MM-DD |
| until | string | YYYY-MM-DD |
| fields | array | extra insight fields by name, e.g. product_title, product_price, carousel_product_card_impressions; each row returns them under `fields` |
| filters | array | keep only matching rows, e.g. {field:"campaign.status",operator:"IN",value:["active"]} or {field:"ad.clicks",operator:"GREATER_THAN",value:100} |
| segment | string | extra group-by dimension (at most one). platform splits rows by ChatGPT app or browser (ios_app, android_app, desktop_web, ios_web, android_web, and web for rows from before 2026-09-10, which OpenAI does not split retroactively); it reports delivery metrics only, not conversions |
| adGroupId | string | scope the report to one ad group |
| campaignId | string | scope the report to one campaign; omit for the whole ad account |
| eventNames | array | only these events, e.g. ["order_created"] |
| conversions | boolean | default true: add conversions, CPA, conversion rate and sales where OpenAI allows them. false leaves them out. |
| granularity | string | default daily |
| viewWindowDays | any | default 1; 0 = no views |
| clickWindowDays | any | default 30 |
| attributedEvents | boolean | add events attributed beyond the goal |
| attributionTimeBasis | string | default ad_event_time |
| includeZeroImpressions | boolean | also list campaigns / ad groups / ads with no impressions (unsegmented reports only) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "scope the report to one ad"
},
"sort": {
"type": "array",
"items": {
"type": "object",
"required": [
"field"
],
"properties": {
"field": {
"type": "string",
"description": "field to order rows by, e.g. campaign.spend"
},
"direction": {
"enum": [
"asc",
"desc"
],
"type": "string",
"description": "asc or desc"
}
}
},
"description": "order rows, e.g. {field:\"campaign.spend\",direction:\"desc\"}"
},
"after": {
"type": "string",
"description": "nextAfter from the previous page; limit goes up to 2000 rows"
},
"level": {
"enum": [
"ad_account",
"campaign",
"ad_group",
"ad"
],
"type": "string",
"description": "roll rows up to this level"
},
"limit": {
"type": "number",
"description": "rows per page, up to 2000"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD"
},
"fields": {
"type": "array",
"items": {
"type": "string"
},
"description": "extra insight fields by name, e.g. product_title, product_price, carousel_product_card_impressions; each row returns them under `fields`"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"required": [
"field",
"operator",
"value"
],
"properties": {
"field": {
"type": "string",
"description": "field to filter on, e.g. campaign.status or ad.clicks"
},
"value": {
"description": "list of values for IN; a number for GREATER_THAN or LESS_THAN"
},
"operator": {
"enum": [
"IN",
"GREATER_THAN",
"LESS_THAN"
],
"type": "string",
"description": "IN matches a list; GREATER_THAN and LESS_THAN compare a number"
}
}
},
"description": "keep only matching rows, e.g. {field:\"campaign.status\",operator:\"IN\",value:[\"active\"]} or {field:\"ad.clicks\",operator:\"GREATER_THAN\",value:100}"
},
"segment": {
"enum": [
"product",
"country",
"device",
"platform"
],
"type": "string",
"description": "extra group-by dimension (at most one). platform splits rows by ChatGPT app or browser (ios_app, android_app, desktop_web, ios_web, android_web, and web for rows from before 2026-09-10, which OpenAI does not split retroactively); it reports delivery metrics only, not conversions"
},
"adGroupId": {
"type": "string",
"description": "scope the report to one ad group"
},
"campaignId": {
"type": "string",
"description": "scope the report to one campaign; omit for the whole ad account"
},
"eventNames": {
"type": "array",
"items": {
"type": "string"
},
"description": "only these events, e.g. [\"order_created\"]"
},
"conversions": {
"type": "boolean",
"description": "default true: add conversions, CPA, conversion rate and sales where OpenAI allows them. false leaves them out."
},
"granularity": {
"enum": [
"hourly",
"daily",
"monthly",
"none"
],
"type": "string",
"description": "default daily"
},
"viewWindowDays": {
"anyOf": [
{
"type": "number",
"const": 0
},
{
"type": "number",
"const": 1
}
],
"description": "default 1; 0 = no views"
},
"clickWindowDays": {
"anyOf": [
{
"type": "number",
"const": 7
},
{
"type": "number",
"const": 14
},
{
"type": "number",
"const": 30
}
],
"description": "default 30"
},
"attributedEvents": {
"type": "boolean",
"description": "add events attributed beyond the goal"
},
"attributionTimeBasis": {
"enum": [
"ad_event_time",
"conversion_time"
],
"type": "string",
"description": "default ad_event_time"
},
"includeZeroImpressions": {
"type": "boolean",
"description": "also list campaigns / ad groups / ads with no impressions (unsegmented reports only)"
}
}
}| string |
| conversion_eqs: e.g. TAG, CONVERSIONS_API |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"what"
],
"properties": {
"date": {
"type": "string",
"description": "metrics_ready_state: YYYY-MM-DD"
},
"what": {
"enum": [
"conversion_eqs",
"metrics_ready_state",
"report_templates",
"promotions",
"targeting_templates"
],
"type": "string",
"description": "which data"
},
"bookmark": {
"type": "string",
"description": "the next page, from the last reply"
},
"lookback": {
"enum": [
"1d",
"14d"
],
"type": "string",
"description": "conversion_eqs: default 14d"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"searchQuery": {
"type": "string",
"description": "targeting_templates: by name"
},
"includeSizing": {
"type": "boolean",
"description": "targeting_templates: add the audience size"
},
"sourcePlatform": {
"type": "string",
"description": "conversion_eqs: e.g. WEB"
},
"ingestionSource": {
"type": "string",
"description": "conversion_eqs: e.g. TAG, CONVERSIONS_API"
}
}
}| YYYY-MM-DD (default today) |
| columns | array | Pinterest async metric columns — omit for the standard spend/impressions/clicks/CTR/conversions set |
| adGroupIds | array | limit the report to these ad group ids |
| adAccountId | string | Pinterest ad account id; omit when only one is shared with this profile |
| campaignIds | array | limit the report to these campaign ids |
| granularity | string | time bucket: TOTAL, DAY, HOUR, WEEK or MONTH |
| reportFormat | string | JSON or CSV |
| targetingTypes | array | only valid with a *_TARGETING level |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "limit the report to these ad ids"
},
"level": {
"type": "string",
"description": "ADVERTISER | CAMPAIGN | AD_GROUP | PIN_PROMOTION | KEYWORD | PRODUCT_GROUP | PRODUCT_ITEM (+ _TARGETING variants) — default CAMPAIGN. An unknown level is refused with the list."
},
"since": {
"type": "string",
"description": "YYYY-MM-DD (default 30 days ago)"
},
"token": {
"type": "string",
"description": "RESUME a pending report — pass the token back instead of re-submitting"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD (default today)"
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Pinterest async metric columns — omit for the standard spend/impressions/clicks/CTR/conversions set"
},
"adGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "limit the report to these ad group ids"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id; omit when only one is shared with this profile"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "limit the report to these campaign ids"
},
"granularity": {
"enum": [
"TOTAL",
"DAY",
"HOUR",
"WEEK",
"MONTH"
],
"type": "string",
"description": "time bucket: TOTAL, DAY, HOUR, WEEK or MONTH"
},
"reportFormat": {
"enum": [
"JSON",
"CSV"
],
"type": "string",
"description": "JSON or CSV"
},
"targetingTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "only valid with a *_TARGETING level"
}
}
}| entityTypes |
| array |
| download: up to 6 types |
| outputFormat | string | download: default JSON |
| updatedSince | string | download: a Unix timestamp |
| campaignFilter | object | download: Pinterest’s campaign_filter object |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"action": {
"enum": [
"download",
"upsert",
"status"
],
"type": "string",
"description": "default download"
},
"create": {
"type": "object",
"description": "upsert: objects to create by type, e.g. {\"campaigns\":[...],\"ad_groups\":[...],\"keywords\":[...]}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"update": {
"type": "object",
"description": "upsert: objects to change by type",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"confirm": {
"type": "boolean",
"description": "upsert: required true when it starts delivery or changes a budget or bid"
},
"entityIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "download: only these ids"
},
"requestId": {
"type": "string",
"description": "status: the id download or upsert answered"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id — omit to use the single shared account"
},
"entityTypes": {
"type": "array",
"items": {
"enum": [
"CAMPAIGN",
"AD_GROUP",
"PRODUCT_GROUP",
"AD",
"KEYWORD",
"LABEL",
"SCHEDULE",
"ENTITY_HISTORY"
],
"type": "string"
},
"description": "download: up to 6 types"
},
"outputFormat": {
"enum": [
"JSON",
"CSV"
],
"type": "string",
"description": "download: default JSON"
},
"updatedSince": {
"type": "string",
"description": "download: a Unix timestamp"
},
"campaignFilter": {
"type": "object",
"description": "download: Pinterest’s campaign_filter object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}| since |
| string |
| YYYY-MM-DD, default 30 days ago |
| until | string | YYYY-MM-DD, default today |
| pinIds | array | ad level only, and only TOGETHER with campaignIds — every ad promoting these Pins |
| columns | array | Pinterest metric column names — omit for the standard set for that level |
| adGroupIds | array | REQUIRED for ad-group level — Pinterest has no all-of-them form there |
| adAccountId | string | Pinterest ad account id; omit a list call to see the shared accounts |
| campaignIds | array | REQUIRED for campaign level; also the second half of the ad-level pinIds pair |
| granularity | string | default TOTAL |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED for ad level — Pinterest refuses /ads/analytics without it, unless you pass pinIds AND campaignIds instead"
},
"level": {
"enum": [
"account",
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "which level to report at — omit and it is inferred from the ids you pass (none -> the whole account)"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD, default 30 days ago"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD, default today"
},
"pinIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ad level only, and only TOGETHER with campaignIds — every ad promoting these Pins"
},
"columns": {
"type": "array",
"items": {
"type": "string"
},
"description": "Pinterest metric column names — omit for the standard set for that level"
},
"adGroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED for ad-group level — Pinterest has no all-of-them form there"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id; omit a list call to see the shared accounts"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED for campaign level; also the second half of the ad-level pinIds pair"
},
"granularity": {
"enum": [
"TOTAL",
"DAY",
"HOUR",
"WEEK",
"MONTH"
],
"type": "string",
"description": "default TOTAL"
}
}
}| from list_posthog_surveys |
| questionId | string | one question id from the survey (see list_posthog_surveys with surveyId) |
| excludeArchived | boolean | true = leave out responses to archived questions |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"surveyId"
],
"properties": {
"limit": {
"type": "number",
"description": "1-500, default 100"
},
"since": {
"type": "string",
"description": "ISO timestamp"
},
"until": {
"type": "string",
"description": "ISO timestamp"
},
"offset": {
"type": "number",
"description": "skip this many responses; use the nextOffset from the previous reply"
},
"scoreGte": {
"type": "number",
"description": "needs questionId; e.g. 9 for NPS promoters"
},
"scoreLte": {
"type": "number",
"description": "needs questionId; e.g. 6 for NPS detractors"
},
"surveyId": {
"type": "string",
"description": "from list_posthog_surveys"
},
"questionId": {
"type": "string",
"description": "one question id from the survey (see list_posthog_surveys with surveyId)"
},
"excludeArchived": {
"type": "boolean",
"description": "true = leave out responses to archived questions"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "preview an existing ad"
},
"adText": {
"type": "string",
"description": "1-100 characters (video preview)"
},
"videoId": {
"type": "string",
"description": "preview a video before building the ad (from upload_tiktok_ads_creative)"
},
"language": {
"enum": [
"ENGLISH",
"CHINESE",
"JAPANESE"
],
"type": "string",
"description": "ENGLISH, CHINESE or JAPANESE; language of the preview"
},
"placement": {
"enum": [
"PLACEMENT_TIKTOK",
"PLACEMENT_PANGLE",
"ALL"
],
"type": "string",
"description": "existing ad only; default TikTok"
},
"displayName": {
"type": "string",
"description": "the brand name shown on the ad, 1-40 characters (video preview)"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"callToAction": {
"type": "string",
"description": "default LEARN_MORE (video preview)"
}
}
}| Argument | Type | Description |
|---|---|---|
| confirm | boolean | REQUIRED to publish. Without it nothing is published and you get the blast-radius report instead. |
| workspace | string | older name for gtmWorkspace (MCP only) |
| versionName | string | name for the version being created; defaults to a timestamped Hermoso name |
| versionPath | string | ROLLBACK / RE-PUBLISH: publish this EXISTING version instead of the workspace. From list_tag_manager_versions. Must be a version inside the same container. |
| gtmWorkspace | string | Tag Manager workspace id or name; defaults to the Default Workspace |
| versionNotes | string | notes stored on the version, visible in Tag Manager |
| containerPath* | string | the "accounts/<id>/containers/<id>" path from list_tag_manager_containers |
| confirmPublicId | string | the container's GTM-XXXXXXX public id, REQUIRED when the container already has a live version. Read it from the preview; it cannot be guessed. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"containerPath"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED to publish. Without it nothing is published and you get the blast-radius report instead."
},
"workspace": {
"type": "string",
"description": "older name for gtmWorkspace (MCP only)"
},
"versionName": {
"type": "string",
"description": "name for the version being created; defaults to a timestamped Hermoso name"
},
"versionPath": {
"type": "string",
"description": "ROLLBACK / RE-PUBLISH: publish this EXISTING version instead of the workspace. From list_tag_manager_versions. Must be a version inside the same container."
},
"gtmWorkspace": {
"type": "string",
"description": "Tag Manager workspace id or name; defaults to the Default Workspace"
},
"versionNotes": {
"type": "string",
"description": "notes stored on the version, visible in Tag Manager"
},
"containerPath": {
"type": "string",
"description": "the \"accounts/<id>/containers/<id>\" path from list_tag_manager_containers"
},
"confirmPublicId": {
"type": "string",
"description": "the container's GTM-XXXXXXX public id, REQUIRED when the container already has a live version. Read it from the preview; it cannot be guessed."
}
}
}| string |
| ascending (default) or descending |
| databaseId | string | database id or link (list_notion_pages type:"database") |
| dataSourceId | string | data source id from an earlier read, as an alternative to databaseId |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1-100, default 50"
},
"match": {
"type": "object",
"description": "{\"Column\": value} — equality for status/select/checkbox/number/date, contains for text/multi-select",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"cursor": {
"type": "string",
"description": "nextCursor from the previous page of rows"
},
"filter": {
"type": "object",
"description": "a raw Notion filter object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"sortBy": {
"type": "string",
"description": "column name to sort by, or created_time or last_edited_time"
},
"archived": {
"type": "boolean",
"description": "read archived rows instead"
},
"direction": {
"enum": [
"ascending",
"descending"
],
"type": "string",
"description": "ascending (default) or descending"
},
"databaseId": {
"type": "string",
"description": "database id or link (list_notion_pages type:\"database\")"
},
"dataSourceId": {
"type": "string",
"description": "data source id from an earlier read, as an alternative to databaseId"
}
}
}| formId |
| string |
| every lead this form has ever collected (from list_meta_lead_forms) |
| pageId | string | which connected Page (required only if the profile has more than one) |
| redact | boolean | mask the contact values, keeping counts / dates / attribution — use when the user only wants to know the form is working |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "just the leads this ONE ad produced (from list_meta_ads) — pass this OR formId, never both"
},
"limit": {
"type": "number",
"description": "1–500, default 100"
},
"since": {
"type": "string",
"description": "ISO date (2026-08-01) or epoch ms — only leads created after this"
},
"until": {
"type": "string",
"description": "ISO date or epoch ms — only leads created before this"
},
"cursor": {
"type": "string",
"description": "paging cursor returned by a previous call"
},
"formId": {
"type": "string",
"description": "every lead this form has ever collected (from list_meta_lead_forms)"
},
"pageId": {
"type": "string",
"description": "which connected Page (required only if the profile has more than one)"
},
"redact": {
"type": "boolean",
"description": "mask the contact values, keeping counts / dates / attribution — use when the user only wants to know the form is working"
}
}
}| budget object: {type DAILY or LIFETIME, amount, currency} |
| status | string | ACTIVE, PAUSED, SCHEDULED, INACTIVE, DELETED_EXTERNALLY or UNDEFINED; default ACTIVE |
| tactic | string | Shopify marketing tactic, e.g. AD, RETARGETING, POST, NEWSLETTER; default AD |
| adSpend | object | amount spent so far |
| channel | string | SOCIAL, SEARCH, DISPLAY, EMAIL or REFERRAL; default SOCIAL |
| platform | string | where it runs, e.g. meta, google_ads, tiktok_ads; only used to derive remoteId |
| remoteId | string | your stable id for this activity (e.g. the ad platform campaign id); derived from platform + campaignId or the utm campaign when omitted |
| remoteUrl | string | https link where this ad or campaign is managed or viewed; required on the first recording |
| campaignId | string | the ad platform campaign id; only used to derive remoteId |
| scheduledEnd | string | ISO date the activity is scheduled to end |
| previewImageUrl | string | https image shown next to the activity (the ad creative works) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"end": {
"type": "string",
"description": "ISO date it ended"
},
"utm": {
"type": "object",
"required": [
"campaign",
"source",
"medium"
],
"properties": {
"medium": {
"type": "string",
"description": "utm_medium value the ad's links carry"
},
"source": {
"type": "string",
"description": "utm_source value the ad's links carry"
},
"campaign": {
"type": "string",
"description": "utm_campaign value the ad's links carry; Shopify matches visits on all three UTM fields"
}
},
"description": "the UTM the ad links carry; Shopify matches visits on all three"
},
"start": {
"type": "string",
"description": "ISO date the ad started"
},
"title": {
"type": "string",
"description": "what the merchant sees in Shopify > Marketing; required on the first recording"
},
"budget": {
"type": "object",
"required": [
"amount",
"currency"
],
"properties": {
"type": {
"enum": [
"DAILY",
"LIFETIME"
],
"type": "string",
"description": "DAILY or LIFETIME"
},
"amount": {
"type": "number",
"description": "budget amount, in the currency given"
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code, e.g. USD"
}
},
"description": "budget object: {type DAILY or LIFETIME, amount, currency}"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"SCHEDULED",
"INACTIVE",
"DELETED_EXTERNALLY",
"UNDEFINED"
],
"type": "string",
"description": "ACTIVE, PAUSED, SCHEDULED, INACTIVE, DELETED_EXTERNALLY or UNDEFINED; default ACTIVE"
},
"tactic": {
"enum": [
"AD",
"RETARGETING",
"POST",
"LINK",
"AFFILIATE",
"MESSAGE",
"NEWSLETTER",
"ABANDONED_CART",
"LOYALTY",
"NOTIFICATION",
"SEO",
"STOREFRONT_APP",
"TRANSACTIONAL"
],
"type": "string",
"description": "Shopify marketing tactic, e.g. AD, RETARGETING, POST, NEWSLETTER; default AD"
},
"adSpend": {
"type": "object",
"required": [
"amount",
"currency"
],
"properties": {
"amount": {
"type": "number",
"description": "MAJOR units: 40 means 40.00"
},
"currency": {
"type": "string",
"description": "3-letter, e.g. USD"
}
},
"description": "amount spent so far"
},
"channel": {
"enum": [
"SOCIAL",
"SEARCH",
"DISPLAY",
"EMAIL",
"REFERRAL"
],
"type": "string",
"description": "SOCIAL, SEARCH, DISPLAY, EMAIL or REFERRAL; default SOCIAL"
},
"platform": {
"type": "string",
"description": "where it runs, e.g. meta, google_ads, tiktok_ads; only used to derive remoteId"
},
"remoteId": {
"type": "string",
"description": "your stable id for this activity (e.g. the ad platform campaign id); derived from platform + campaignId or the utm campaign when omitted"
},
"remoteUrl": {
"type": "string",
"description": "https link where this ad or campaign is managed or viewed; required on the first recording"
},
"campaignId": {
"type": "string",
"description": "the ad platform campaign id; only used to derive remoteId"
},
"scheduledEnd": {
"type": "string",
"description": "ISO date the activity is scheduled to end"
},
"previewImageUrl": {
"type": "string",
"description": "https image shown next to the activity (the ad creative works)"
}
}
}| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| bidStrategy | string | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"budget"
],
"properties": {
"budget": {
"type": "number",
"description": "budget in the ad account’s currency"
},
"bidType": {
"enum": [
"CPC",
"CPM",
"CPV",
"CPV6",
"CPV15"
],
"type": "string",
"description": "default CPC — must fit the campaign objective"
},
"endTime": {
"type": "string",
"description": "ISO 8601 end of the flight to price"
},
"currency": {
"type": "string",
"description": "currency code, e.g. USD; returned amounts use the ad account currency"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "DAILY_SPEND or LIFETIME_SPEND"
},
"objective": {
"enum": [
"APP_INSTALLS",
"BRAND_AWARENESS",
"CATALOG_SALES",
"CLICKS",
"CONVERSIONS",
"IMPRESSIONS",
"LEAD_GENERATION",
"SALES",
"VIDEO_VIEWABLE_IMPRESSIONS"
],
"type": "string",
"description": "default CLICKS"
},
"startTime": {
"type": "string",
"description": "ISO 8601 start of the flight to price"
},
"targeting": {
"type": "object",
"description": "targeting block, same shape as create_reddit_ads_ad_group targeting",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"bidStrategy": {
"enum": [
"BIDLESS",
"MANUAL_BIDDING",
"MAXIMIZE_VOLUME",
"TARGET_CPX"
],
"type": "string",
"description": "BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"jobId": {
"type": "string",
"description": "check an existing job instead of creating one"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved — deletion is irreversible"
},
"pixelId": {
"type": "string",
"description": "limit the deletion to one pixel’s data (from list_reddit_ads_pixels)"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"identifiers": {
"type": "array",
"items": {
"type": "object",
"required": [
"type",
"value"
],
"properties": {
"type": {
"enum": [
"EMAIL",
"PHONE_NUMBER",
"IDFA",
"AAID",
"EXTERNAL_ID"
],
"type": "string",
"description": "EMAIL, PHONE_NUMBER, IDFA, AAID or EXTERNAL_ID"
},
"value": {
"type": "string",
"description": "the identifier in plain form, or a 64-char SHA-256 hash"
}
}
},
"description": "who to delete; omit when checking a jobId"
}
}
}| object |
| same shape as create_reddit_ads_ad_group targeting |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| bidStrategy | string | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"budget"
],
"properties": {
"budget": {
"type": "number",
"description": "budget in the ad account’s currency (not micro-currency — the conversion is handled)"
},
"bidType": {
"enum": [
"CPC",
"CPM",
"CPV",
"CPV6",
"CPV15"
],
"type": "string",
"description": "CPC, CPM, CPV, CPV6 or CPV15; must fit the objective"
},
"endTime": {
"type": "string",
"description": "ISO 8601 end of the forecast window"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "DAILY_SPEND or LIFETIME_SPEND; LIFETIME_SPEND needs endTime"
},
"bidAmount": {
"type": "number",
"description": "bid in the ad account currency; needed for paid bid types"
},
"objective": {
"enum": [
"APP_INSTALLS",
"BRAND_AWARENESS",
"CATALOG_SALES",
"CLICKS",
"CONVERSIONS",
"IMPRESSIONS",
"LEAD_GENERATION",
"SALES",
"VIDEO_VIEWABLE_IMPRESSIONS"
],
"type": "string",
"description": "default CLICKS"
},
"startTime": {
"type": "string",
"description": "ISO 8601"
},
"targeting": {
"type": "object",
"description": "same shape as create_reddit_ads_ad_group targeting",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"bidStrategy": {
"enum": [
"BIDLESS",
"MANUAL_BIDDING",
"MAXIMIZE_VOLUME",
"TARGET_CPX"
],
"type": "string",
"description": "BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX"
}
}
}| array |
| limit to these change kinds, e.g. BUDGET, BID, STATUS, TARGETING |
| includeChildEntities | boolean | also return changes to what lives under those objects |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "default 50, max 200"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD or full ISO timestamp"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD or full ISO timestamp"
},
"entityIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "restrict to these objects"
},
"memberIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "restrict to changes made by these Reddit members"
},
"entityType": {
"enum": [
"AD",
"AD_GROUP",
"CAMPAIGN"
],
"type": "string",
"description": "required when you pass entityIds"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"changeTypes": {
"type": "array",
"items": {
"enum": [
"AD_ACCOUNT",
"AD",
"AD_GROUP",
"AUDIENCE",
"BID",
"BUDGET",
"CAMPAIGN",
"STATUS",
"TARGETING"
],
"type": "string"
},
"description": "limit to these change kinds, e.g. BUDGET, BID, STATUS, TARGETING"
},
"includeChildEntities": {
"type": "boolean",
"description": "also return changes to what lives under those objects"
}
}
}| object |
| same shape as create_reddit_ads_ad_group targeting |
| useCatalog | boolean | SALES objective: true forecasts catalog sales |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| bidStrategy | string | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"objective",
"goalType",
"budget"
],
"properties": {
"budget": {
"type": "number",
"description": "budget in the ad account’s currency (not micro-currency)"
},
"bidType": {
"enum": [
"CPC",
"CPM",
"CPV",
"CPV6",
"CPV15"
],
"type": "string",
"description": "CPC, CPM, CPV, CPV6 or CPV15"
},
"endTime": {
"type": "string",
"description": "ISO 8601; default 30 days after the start, at most 365"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "whether budget is per day or for the whole plan"
},
"bidAmount": {
"type": "number",
"description": "bid in the ad account currency"
},
"objective": {
"enum": [
"APP_INSTALLS",
"BRAND_AWARENESS",
"CATALOG_SALES",
"CLICKS",
"CONVERSIONS",
"IMPRESSIONS",
"LEAD_GENERATION",
"SALES",
"VIDEO_VIEWABLE_IMPRESSIONS"
],
"type": "string",
"description": "campaign objective"
},
"startTime": {
"type": "string",
"description": "ISO 8601; default now"
},
"targeting": {
"type": "object",
"description": "same shape as create_reddit_ads_ad_group targeting",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"useCatalog": {
"type": "boolean",
"description": "SALES objective: true forecasts catalog sales"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"bidStrategy": {
"enum": [
"BIDLESS",
"MANUAL_BIDDING",
"MAXIMIZE_VOLUME",
"TARGET_CPX"
],
"type": "string",
"description": "BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX"
}
}
}| breakdowns | array | up to 3 (4 if both COUNTRY and REGION). Omit for one aggregate row. |
| timeZoneId | string | IANA zone, e.g. America/New_York |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"since": {
"type": "string",
"description": "YYYY-MM-DD or full ISO timestamp, default 30 days ago"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD or full ISO timestamp, default today"
},
"fields": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"filter": {
"type": "string",
"description": "Reddit filter expression to restrict rows to particular ids/values"
},
"breakdowns": {
"type": "array",
"items": {
"enum": [
"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"
],
"type": "string"
},
"description": "up to 3 (4 if both COUNTRY and REGION). Omit for one aggregate row."
},
"timeZoneId": {
"type": "string",
"description": "IANA zone, e.g. America/New_York"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}| tiktokItemId* | string | the TikTok video id, from the same comment row |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"commentId",
"adId",
"tiktokItemId",
"identityId",
"identityType",
"text"
],
"properties": {
"adId": {
"type": "string",
"description": "from the same comment row"
},
"text": {
"type": "string",
"description": "the reply, exactly as it will appear in public"
},
"commentId": {
"type": "string",
"description": "the comment being replied to — from list_tiktok_ads_comments"
},
"identityId": {
"type": "string",
"description": "from the same comment row (or list_tiktok_ads_identities) — the account the reply is posted AS"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"identityType": {
"enum": [
"TT_USER",
"CUSTOMIZED_USER"
],
"type": "string",
"description": "as reported on the comment row"
},
"tiktokItemId": {
"type": "string",
"description": "the TikTok video id, from the same comment row"
}
}
}| source |
| string |
| "generated" (default) = an AI-made person; "upload" / "social" = a REAL person |
| useAnyway | boolean | Only for a creator whose saved photo was flagged too unclear to cast: true casts the current photo as it is, no new image needed |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"look": {
"type": "string",
"description": "their canonical wardrobe/appearance in words — reused to hold the look steady across ads"
},
"name": {
"type": "string",
"description": "what to call this creator (e.g. “Sarah”) — list_creators and the app’s picker match on it"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"image": {
"type": "string",
"description": "REQUIRED except with useAnyway. public https url of the portrait (an existing render’s url, or any public photo). Not a local file path — upload it with upload_file first and save the url that returns"
},
"poses": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 4 extra full-body / angle plates of the SAME person (public urls) — they make a wider shot hold the identity"
},
"voice": {
"type": "string",
"description": "a default voice name for this persona (engines + voices are in hermoso_capabilities)"
},
"source": {
"enum": [
"generated",
"upload",
"social"
],
"type": "string",
"description": "\"generated\" (default) = an AI-made person; \"upload\" / \"social\" = a REAL person"
},
"useAnyway": {
"type": "boolean",
"description": "Only for a creator whose saved photo was flagged too unclear to cast: true casts the current photo as it is, no new image needed"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"icon": {
"type": "string",
"description": "one emoji"
},
"title": {
"type": "string",
"description": "title of the new Notion page"
},
"content": {
"type": "string",
"description": "markdown body"
},
"databaseId": {
"type": "string",
"description": "database id or link: saves as a new row"
},
"dataSourceId": {
"type": "string",
"description": "data source id to save into as a new row; pass only one of parentPageId, databaseId, dataSourceId"
},
"parentPageId": {
"type": "string",
"description": "page id or link to save under"
},
"includeSchedule": {
"type": "boolean",
"description": "add the scheduled posts"
},
"swipefileCollection": {
"type": "string",
"description": "swipefile collection name or id"
}
}
}| storeFronts | array | App Store country/region codes to search in, e.g. ["US","GB"]. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cpids": {
"type": "string",
"description": "Comma-separated iTunes content provider ids to scope the search to."
},
"limit": {
"type": "number",
"description": "maximum number of apps to return"
},
"query": {
"type": "string",
"description": "App name or developer to search the App Store for."
},
"adamId": {
"type": "string",
"description": "Fetch one app’s full details instead of searching."
},
"offset": {
"type": "number",
"description": "apps to skip, for paging"
},
"ownedOnly": {
"type": "boolean",
"description": "Default true — only apps this organization owns (the only ones that can be promoted). false searches the whole catalogue. Apple’s own default is false; Hermoso flips it because only an owned app can be advertised."
},
"storeFronts": {
"type": "array",
"items": {
"type": "string"
},
"description": "App Store country/region codes to search in, e.g. [\"US\",\"GB\"]."
}
}
}| geo locations to skip, for paging |
| eligible | boolean | false excludes soft-blocked geos entirely. Default includes them with eligibility data. |
| countryCode | string | ISO 3166-1 alpha-2 to scope the search. |
| supplySource* | string | REQUIRED. APPSTORE for App Store campaigns, MAPS for Apple Maps. Decides which entity types exist. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"supplySource"
],
"properties": {
"geos": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "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": {
"type": "number",
"description": "maximum number of geo locations to return"
},
"query": {
"type": "string",
"description": "Name to search, minimum 2 characters. \"*\" returns everything at the entity level."
},
"entity": {
"enum": [
"Country",
"AdminArea",
"Locality",
"PostalCode"
],
"type": "string",
"description": "Narrow to one granularity."
},
"offset": {
"type": "number",
"description": "geo locations to skip, for paging"
},
"eligible": {
"type": "boolean",
"description": "false excludes soft-blocked geos entirely. Default includes them with eligibility data."
},
"countryCode": {
"type": "string",
"description": "ISO 3166-1 alpha-2 to scope the search."
},
"supplySource": {
"enum": [
"APPSTORE",
"MAPS"
],
"type": "string",
"description": "REQUIRED. APPSTORE for App Store campaigns, MAPS for Apple Maps. Decides which entity types exist."
}
}
}| rowLimit | number | 1-25000, default 1000 |
| startRow | number | zero-based, for paging past rowLimit |
| dataState | string | final (default, lags 2-3 days), all (includes fresh partial data that WILL change), or hourlyAll |
| startDate* | string | YYYY-MM-DD, required, Pacific time, inclusive |
| dimensions | array | group by any of query, page, country, device, searchAppearance, date, hour. Omit for one total row. An unknown name is refused by name rather than dropped. |
| aggregationType | string | auto (default), byPage, byProperty or byNewsShowcasePanel |
| dimensionFilterGroups | array | narrow the report, e.g. [{"filters":[{"dimension":"query","operator":"contains","expression":"buy"}]}]. dimension is one of query/page/country/device/searchAppearance — date and hour are NOT filterable, they are the date range. operator is equals/notEquals/contains/notContains/includingRegex/excludingRegex. An empty expression is refused: Google matches it literally, so it would silently return nothing. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"startDate",
"endDate"
],
"properties": {
"type": {
"type": "string",
"description": "web (default), image, video, news, discover or googleNews"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD, required, Pacific time, inclusive"
},
"siteUrl": {
"type": "string",
"description": "the exact property string from list_search_console_sites — \"sc-domain:example.com\" or \"https://example.com/\", never a bare domain, and it must be one SHARED with this profile"
},
"rowLimit": {
"type": "number",
"description": "1-25000, default 1000"
},
"startRow": {
"type": "number",
"description": "zero-based, for paging past rowLimit"
},
"dataState": {
"type": "string",
"description": "final (default, lags 2-3 days), all (includes fresh partial data that WILL change), or hourlyAll"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD, required, Pacific time, inclusive"
},
"dimensions": {
"type": "array",
"items": {
"type": "string"
},
"description": "group by any of query, page, country, device, searchAppearance, date, hour. Omit for one total row. An unknown name is refused by name rather than dropped."
},
"aggregationType": {
"type": "string",
"description": "auto (default), byPage, byProperty or byNewsShowcasePanel"
},
"dimensionFilterGroups": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "narrow the report, e.g. [{\"filters\":[{\"dimension\":\"query\",\"operator\":\"contains\",\"expression\":\"buy\"}]}]. dimension is one of query/page/country/device/searchAppearance — date and hour are NOT filterable, they are the date range. operator is equals/notEquals/contains/notContains/includingRegex/excludingRegex. An empty expression is refused: Google matches it literally, so it would silently return nothing."
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "number",
"description": "1–500, default 100"
},
"query": {
"type": "string",
"description": "narrow the list to options mentioning this text (filtered here, not by Pinterest)"
},
"interestId": {
"type": "string",
"description": "drill INTO one interest and return its child interests — how you walk the interest tree"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id; omit when only one is shared with this profile"
},
"targetingType": {
"enum": [
"APPTYPE",
"GENDER",
"LOCALE",
"AGE_BUCKET",
"LOCATION",
"GEO",
"INTEREST",
"KEYWORD",
"AUDIENCE_INCLUDE",
"AUDIENCE_EXCLUDE"
],
"type": "string",
"description": "which catalog to list — required unless you pass interestId"
}
}
}| 2-letter country code, geolocations only |
| keywords | array | keywords / keyword_check: the terms (up to 1,000, each ≤100 characters) |
| postalCode | string | postal code to narrow a geolocations search |
| websiteUrl | string | community_suggestions: suggest communities related to this site |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"communities",
"community_suggestions",
"keywords",
"keyword_check",
"geolocations",
"interests",
"devices",
"carriers",
"languages",
"third_party_audiences"
],
"type": "string",
"description": "default communities"
},
"limit": {
"type": "number",
"description": "max results, default 15"
},
"query": {
"type": "string",
"description": "keyword — required for communities; topics for community_suggestions; seeds for keywords; filters interests / devices / carriers / languages; searches cities for geolocations"
},
"country": {
"type": "string",
"description": "2-letter country code, geolocations only"
},
"keywords": {
"type": "array",
"items": {
"type": "string"
},
"description": "keywords / keyword_check: the terms (up to 1,000, each ≤100 characters)"
},
"postalCode": {
"type": "string",
"description": "postal code to narrow a geolocations search"
},
"websiteUrl": {
"type": "string",
"description": "community_suggestions: suggest communities related to this site"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}| advertiserId | string | from list_tiktok_ads_accounts — TikTok scopes these lookups to an advertiser |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"enum": [
"location",
"interest",
"interest_keyword",
"hashtag",
"language"
],
"type": "string",
"description": "default location. interest_keyword and hashtag both REQUIRE keyword."
},
"level": {
"enum": [
"country",
"province",
"city",
"district"
],
"type": "string",
"description": "location only: return one level"
},
"limit": {
"type": "number",
"description": "rows to return, default 50, max 200; total always counts every match"
},
"keyword": {
"type": "string",
"description": "narrows the lookup by name, e.g. \"Canada\" returns Canada rather than every place TikTok has; REQUIRED for interest_keyword and hashtag"
},
"regionCode": {
"type": "string",
"description": "location only: two-letter country code, e.g. CA, to list the places inside one country"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — TikTok scopes these lookups to an advertiser"
}
}
}| conversionId | string | default conversion rule id for every event that does not name its own |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"events"
],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "the conversion events — each takes conversionId, conversionHappenedAt (epoch ms or ISO, within 90 days), and at least one identifier: email (PLAIN — hashed here, never hash it yourself), emailSha256, liFatId, ipAddress (IPv4, hashed here), googleAid, acxiomId, firstName WITH lastName, companyName, title, countryCode, lead (urn:li:leadGenFormResponse:…), externalIds (max 1), plus optional amount + currencyCode and an eventId for Insight-Tag deduplication"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
},
"conversionId": {
"type": "string",
"description": "default conversion rule id for every event that does not name its own"
}
}
}| conversionName | string | default goal name for every row that does not carry its own |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"conversions"
],
"properties": {
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"conversions": {
"type": "array",
"items": {
"type": "object",
"required": [
"conversionTime"
],
"properties": {
"email": {
"type": "string",
"description": "a PLAIN email address (hashed here), or a SHA-256 digest you already hold"
},
"phone": {
"type": "string",
"description": "E.164 WITH the country code, e.g. +14255551234, or a digest. A number without a country code is refused rather than guessed"
},
"conversionName": {
"type": "string",
"description": "must match an existing Microsoft OFFLINE conversion goal exactly"
},
"conversionTime": {
"type": "string",
"description": "ISO 8601 — within the last 90 days, and later than the click"
},
"conversionValue": {
"type": "number",
"description": "defaults to the goal’s own revenue setting"
},
"microsoftClickId": {
"type": "string",
"description": "the msclkid. Optional only when an email or phone is supplied"
},
"conversionCurrencyCode": {
"type": "string",
"description": "3-letter ISO code; defaults to the goal’s currency"
},
"externalAttributionModel": {
"type": "string",
"description": "name of the external attribution model; only with externalAttributionCredit"
},
"externalAttributionCredit": {
"type": "number",
"description": "the fraction of the conversion this click gets, >0 and ≤1. Only for goals configured for external attribution, and only together with externalAttributionModel"
}
}
},
"description": "up to 1000 per request — Microsoft applies each request independently"
},
"conversionName": {
"type": "string",
"description": "default goal name for every row that does not carry its own"
}
}
}| Description |
|---|
| apiKey* | string | the Conversions API key (create_openai_ads_conversion_api_key) — NOT the Advertiser API key the connector stores |
| events* | array | 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 |
| pixelSnippetId* | string | OpenAI’s pixel_id — create_openai_ads_pixel returns it as pixelSnippetId. NOT the clidsrc_… pixelId, which is the conversion source id. |
| integrationSource | string | stable identifier for the integration sending the batch; defaults to "hermoso" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"pixelSnippetId",
"apiKey",
"events"
],
"properties": {
"apiKey": {
"type": "string",
"description": "the Conversions API key (create_openai_ads_conversion_api_key) — NOT the Advertiser API key the connector stores"
},
"events": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "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": {
"type": "boolean",
"description": "true validates the batch against ChatGPT Ads and SAVES NOTHING — no conversion is recorded"
},
"pixelSnippetId": {
"type": "string",
"description": "OpenAI’s pixel_id — create_openai_ads_pixel returns it as pixelSnippetId. NOT the clidsrc_… pixelId, which is the conversion source id."
},
"integrationSource": {
"type": "string",
"description": "stable identifier for the integration sending the batch; defaults to \"hermoso\""
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"events"
],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"aaid": {
"type": "string",
"description": "Android advertising id"
},
"idfa": {
"type": "string",
"description": "iOS advertising id"
},
"uuid": {
"type": "string",
"description": "the first-party _rdt_uuid cookie value"
},
"email": {
"type": "string",
"description": "plain address or a 64-char SHA-256 hash"
},
"phone": {
"type": "string",
"description": "E.164 like +15554441234, or a 64-char SHA-256 hash"
},
"ipAddress": {
"type": "string",
"description": "customer IP address; send with userAgent"
},
"userAgent": {
"type": "string",
"description": "browser user-agent string; send with ipAddress"
},
"externalId": {
"type": "string",
"description": "your own customer id"
},
"screenWidth": {
"type": "number",
"description": "screen width in pixels"
},
"screenHeight": {
"type": "number",
"description": "screen height in pixels"
},
"limitedDataUse": {
"type": "object",
"required": [
"country"
],
"properties": {
"region": {
"type": "string",
"description": "region code of a Limited Data Use user"
},
"country": {
"type": "string",
"description": "country code of a Limited Data Use user (required when the object is set)"
}
},
"description": "flag this user as Limited Data Use (they did not consent to behavioural targeting); country is required"
}
},
"description": "customer identifiers used to match each conversion; include as many as you have"
},
"clickId": {
"type": "string",
"description": "Reddit’s own click id, the strongest match key there is"
},
"eventAt": {
"type": [
"number",
"string"
],
"description": "when it happened — ISO timestamp or Unix epoch; defaults to now. Must be within the last 7 days."
},
"metadata": {
"type": "object",
"properties": {
"value": {
"type": "number",
"description": "revenue, in that currency"
},
"currency": {
"type": "string",
"description": "ISO currency code of value, e.g. USD"
},
"products": {
"type": "array",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "product id (SKU)"
},
"name": {
"type": "string",
"description": "product name"
},
"category": {
"type": "string",
"description": "product category"
},
"quantity": {
"type": "number",
"description": "units in the conversion"
},
"itemPrice": {
"type": "number",
"description": "price of one unit, in the event currency"
}
}
},
"description": "products in the conversion, each with at least an id"
},
"itemCount": {
"type": "number",
"description": "number of items in the conversion"
},
"conversionId": {
"type": "string",
"description": "YOUR unique id for this conversion — the deduplication key; use the order number for purchases"
}
},
"description": "conversion details: conversionId, value, currency, itemCount, products"
},
"actionSource": {
"enum": [
"WEBSITE",
"APP",
"OTHER",
"PHYSICAL_STORE"
],
"type": "string",
"description": "default WEBSITE — where the conversion happened"
},
"trackingType": {
"enum": [
"PAGE_VISIT",
"VIEW_CONTENT",
"SEARCH",
"ADD_TO_CART",
"ADD_TO_WISHLIST",
"PURCHASE",
"LEAD",
"SIGN_UP",
"CUSTOM"
],
"type": "string",
"description": "default PAGE_VISIT"
},
"eventSourceUrl": {
"type": "string",
"description": "the page the conversion happened on"
},
"customEventName": {
"type": "string",
"description": "required when trackingType is CUSTOM — free-form, CASE-SENSITIVE, max 64 chars; only the 20 most recent custom events show on Reddit’s dashboard"
}
}
},
"description": "up to 1,000 events per call"
},
"testId": {
"type": "string",
"description": "a test id from Events Manager > Testing — events sent with it are visible there and NEVER counted in reporting"
},
"pixelId": {
"type": "string",
"description": "from list_reddit_ads_pixels — only needed when the account has more than one"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"events"
],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"required": [
"eventName"
],
"properties": {
"user": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "email address; hashed before it is sent"
},
"madid": {
"type": "string",
"description": "mobile advertising id"
},
"phone": {
"type": "string",
"description": "phone number with country code, e.g. +1 555 0100; hashed before it is sent"
},
"leadId": {
"type": "string",
"description": "a Snapchat lead id from list_snapchat_ads_leads"
},
"clickId": {
"type": "string",
"description": "the ScCid value"
},
"cookie1": {
"type": "string",
"description": "the _scid cookie"
},
"lastName": {
"type": "string",
"description": "last name; hashed before it is sent"
},
"firstName": {
"type": "string",
"description": "first name; hashed before it is sent"
},
"ipAddress": {
"type": "string",
"description": "IP address of the person; send with userAgent"
},
"userAgent": {
"type": "string",
"description": "browser user agent string; send with ipAddress"
},
"externalId": {
"type": "string",
"description": "your own customer id for the person"
}
},
"description": "matching data for the person; at least one matchable field is needed"
},
"eventId": {
"type": "string",
"description": "dedup key shared with the pixel (order id, transaction id)"
},
"eventName": {
"type": "string",
"description": "PURCHASE, SIGN_UP, PAGE_VIEW, ADD_CART, START_CHECKOUT, VIEW_CONTENT, SUBSCRIBE, START_TRIAL, … or CUSTOM_EVENT_1..5"
},
"eventTime": {
"type": [
"number",
"string"
],
"description": "ISO or epoch; default now; within the last 7 days"
},
"customData": {
"type": "object",
"properties": {
"value": {
"type": [
"number",
"string"
],
"description": "event value in currency; required for PURCHASE"
},
"orderId": {
"type": "string",
"description": "order id, used to deduplicate purchases"
},
"contents": {
"type": "array",
"items": {},
"description": "product items array, passed through as Snapchat expects"
},
"currency": {
"type": "string",
"description": "ISO currency code, e.g. USD; required for PURCHASE"
}
},
"description": "event value and order details, e.g. for a PURCHASE"
},
"actionSource": {
"enum": [
"WEB",
"OFFLINE",
"MOBILE_APP"
],
"type": "string",
"description": "default WEB"
},
"eventSourceUrl": {
"type": "string",
"description": "REQUIRED for WEB: the page it happened on (a URL carrying ScCid also attributes the click)"
},
"limitedDataUse": {
"type": "boolean",
"description": "the person opted out (LMU)"
}
}
},
"description": "conversion events to send, up to 2,000 per call"
},
"pixelId": {
"type": "string",
"description": "website / offline events"
},
"snapAppId": {
"type": "string",
"description": "app events (Snap App ID)"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"validateOnly": {
"type": "boolean",
"description": "Snapchat validates and records nothing"
},
"testEventCode": {
"type": "string",
"description": "label for Events Manager testing"
}
}
}| TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| eventSourceId* | string | web: the pixel CODE from list_tiktok_ads_pixels (NOT the pixelId) · app: the TikTok App ID · offline: the Offline Event Set ID · crm: the CRM Event Set ID |
| testEventCode | string | from Events Manager > Test Events. WITH it nothing reaches reporting or optimisation; WITHOUT it these are real permanent conversions. Offer it before sending anything live. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"eventSource",
"eventSourceId",
"events"
],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"required": [
"event",
"eventTime"
],
"properties": {
"ad": {
"type": "object",
"description": "optional TikTok ad-attribution object (for example a callback value), passed through as sent",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"app": {
"type": "object",
"description": "REQUIRED for app events",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"lead": {
"type": "object",
"description": "REQUIRED for crm events",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"page": {
"type": "object",
"description": "{ url, referrer } — REQUIRED for web events",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"user": {
"type": "object",
"description": "{ email, phone, external_id, ttclid, ttp, ip, user_agent } — email/phone/external_id are hashed for you",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"event": {
"type": "string",
"description": "a TikTok standard event (CompletePayment, Purchase, AddToCart, CompleteRegistration, SubmitForm, ViewContent …) or your own custom name"
},
"eventId": {
"type": "string",
"description": "REQUIRED if the same event is also sent by the browser pixel — TikTok deduplicates on event_source_id + event_id + event"
},
"eventTime": {
"type": "number",
"description": "Unix timestamp in SECONDS, UTC"
},
"properties": {
"type": "object",
"description": "{ value, currency, contents:[{content_id, content_name, price, quantity}], order_id … }",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"limitedDataUse": {
"type": "boolean",
"description": "true marks the event as limited data use, for a person who opted out of data processing"
}
}
},
"description": "up to 1000 per request — above that TikTok rejects the WHOLE request, not the overflow"
},
"eventSource": {
"enum": [
"web",
"app",
"offline",
"crm"
],
"type": "string",
"description": "web = a website pixel · app = a mobile app (TikTok gates this behind an allowlist) · offline = a physical store · crm = lead events from a CRM"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"eventSourceId": {
"type": "string",
"description": "web: the pixel CODE from list_tiktok_ads_pixels (NOT the pixelId) · app: the TikTok App ID · offline: the Offline Event Set ID · crm: the CRM Event Set ID"
},
"testEventCode": {
"type": "string",
"description": "from Events Manager > Test Events. WITH it nothing reaches reporting or optimisation; WITHOUT it these are real permanent conversions. Offer it before sending anything live."
}
}
}| eventSetId* | string | the Offline Event Set these belong to — from list_tiktok_ads_offline_event_sets |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"eventSetId",
"events"
],
"properties": {
"events": {
"type": "array",
"items": {
"type": "object",
"required": [
"event",
"timestamp",
"user"
],
"properties": {
"user": {
"type": "object",
"description": "{ emails: [...], phone_numbers: [...] } — at least one is REQUIRED. Pass them in the clear and Hermoso hashes them, or pass SHA-256 hashes you already hold.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"event": {
"type": "string",
"description": "one of TikTok’s supported offline events, CASE SENSITIVE: Purchase, Contact, Subscribe, Lead, AddPaymentInfo, AddToCart, ApplicationApproval, AddToWishlist, CompleteRegistration, Download, InitiateCheckout, Search, ViewContent, StartTrial, SubmitApplication, CustomizeProduct, FindLocation, Schedule. TikTok publishes a CLOSED list here and no custom-event mechanism, unlike web events."
},
"eventId": {
"type": "string",
"description": "your own unique id for this event, so it can be matched between your system and TikTok"
},
"timestamp": {
"type": "string",
"description": "ISO-8601 STRING, e.g. \"2026-08-19T19:11:01Z\" — NOT a Unix number"
},
"properties": {
"type": "object",
"description": "{ value, currency, order_id, shop_id, contents: [{content_id, content_name, price, quantity}], event_channel: in_store | phone_call | email | website | crm | other }. A value with no currency is refused — TikTok marks both required for revenue reporting, and a figure with no unit is a wrong number rather than a missing one.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
},
"description": "one event goes to TikTok’s single endpoint, several to its bulk one — Hermoso picks"
},
"eventSetId": {
"type": "string",
"description": "the Offline Event Set these belong to — from list_tiktok_ads_offline_event_sets"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}| audiences | array | audience ids from manage_google_ads_audience (create_audience) for Demand Gen or Performance Max |
| interests | array | interest ids (affinity or in-market) to target on the ad group |
| userLists | array | user list ids to target on the ad group |
| campaignId | string | campaign id; exclusions only |
| customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
| excludeAges | array | age ranges to exclude: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown |
| assetGroupId | string | Performance Max asset group id; audiences become signals there |
| searchThemes | array | search theme phrases for a Performance Max asset group, up to 50, 10 words or fewer each |
| customSegments | array | custom segment ids to target; Display ad groups only, Search refuses them |
| excludeGenders | array | genders to exclude: male, female or unknown |
| loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"mode": {
"enum": [
"observation",
"targeting"
],
"type": "string",
"description": "observation (report only, default on Search) or targeting (narrow delivery to these audiences)"
},
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and change nothing"
},
"remove": {
"type": "array",
"items": {
"type": "string"
},
"description": "criterion resource names to remove; a call with no other changes lists the current ones"
},
"confirm": {
"type": "boolean",
"description": "true to approve a change that alters who a live ad group reaches"
},
"exclude": {
"type": "object",
"properties": {
"interests": {
"type": "array",
"items": {
"type": "string"
},
"description": "interest ids to exclude"
},
"userLists": {
"type": "array",
"items": {
"type": "string"
},
"description": "user list ids to exclude"
},
"customSegments": {
"type": "array",
"items": {
"type": "string"
},
"description": "custom segment ids to exclude"
}
},
"description": "audiences to exclude from the ad group or campaign: {userLists, interests, customSegments} ids"
},
"adGroupId": {
"type": "string",
"description": "ad group to target (Search, Display or Demand Gen); one of adGroupId, assetGroupId, campaignId"
},
"audiences": {
"type": "array",
"items": {
"type": "string"
},
"description": "audience ids from manage_google_ads_audience (create_audience) for Demand Gen or Performance Max"
},
"interests": {
"type": "array",
"items": {
"type": "string"
},
"description": "interest ids (affinity or in-market) to target on the ad group"
},
"userLists": {
"type": "array",
"items": {
"type": "string"
},
"description": "user list ids to target on the ad group"
},
"campaignId": {
"type": "string",
"description": "campaign id; exclusions only"
},
"customerId": {
"type": "string",
"description": "Google Ads account id, 10 digits (dashes ok); omit for the brand's default account"
},
"excludeAges": {
"type": "array",
"items": {
"type": "string"
},
"description": "age ranges to exclude: 18-24, 25-34, 35-44, 45-54, 55-64, 65+ or unknown"
},
"assetGroupId": {
"type": "string",
"description": "Performance Max asset group id; audiences become signals there"
},
"searchThemes": {
"type": "array",
"items": {
"type": "string"
},
"description": "search theme phrases for a Performance Max asset group, up to 50, 10 words or fewer each"
},
"customSegments": {
"type": "array",
"items": {
"type": "string"
},
"description": "custom segment ids to target; Display ad groups only, Search refuses them"
},
"excludeGenders": {
"type": "array",
"items": {
"type": "string"
},
"description": "genders to exclude: male, female or unknown"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id, when the account is managed through one"
}
}
}| omit to use the brand’s selected default account |
| targetRoas | number | REQUIRED for TARGET_ROAS — e.g. 4 = $4 revenue per $1 spent |
| 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. |
| targetCpaUsd | number | REQUIRED for TARGET_CPA |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"strategy"
],
"properties": {
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and change nothing"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change a LIVE (ENABLED) campaign"
},
"strategy": {
"enum": [
"MANUAL_CPC",
"MAXIMIZE_CLICKS",
"MAXIMIZE_CONVERSIONS",
"MAXIMIZE_CONVERSION_VALUE",
"TARGET_CPA",
"TARGET_ROAS"
],
"type": "string",
"description": "the bidding strategy"
},
"maxCpcUsd": {
"type": "number",
"description": "MAXIMIZE_CLICKS — the max CPC ceiling; REQUIRED when switching an existing campaign to it"
},
"campaignId": {
"type": "string",
"description": "the campaign to change"
},
"customerId": {
"type": "string",
"description": "omit to use the brand’s selected default account"
},
"targetRoas": {
"type": "number",
"description": "REQUIRED for TARGET_ROAS — e.g. 4 = $4 revenue per $1 spent"
},
"enhancedCpc": {
"type": "boolean",
"description": "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."
},
"targetCpaUsd": {
"type": "number",
"description": "REQUIRED for TARGET_CPA"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
}
}
}| boolean |
| put the campaign back on the account goals |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goals": {
"type": "array",
"items": {
"type": "object",
"required": [
"category",
"biddable"
],
"properties": {
"origin": {
"type": "string",
"description": "where the conversion happens, e.g. WEBSITE; default WEBSITE"
},
"biddable": {
"type": "boolean",
"description": "true = smart bidding optimises for this goal"
},
"category": {
"type": "string",
"description": "goal category, e.g. PURCHASE, SIGNUP, SUBMIT_LEAD_FORM or PAGE_VIEW"
}
}
},
"description": "conversion goals to set, as a list of {category, origin, biddable}; omit to read the current goals"
},
"dryRun": {
"type": "boolean",
"description": "validate with Google, change nothing"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved a change to a LIVE campaign or a delete"
},
"campaignId": {
"type": "string",
"description": "omit for the account goals"
},
"customerId": {
"type": "string",
"description": "omit to use the selected default account"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"useAccountGoals": {
"type": "boolean",
"description": "put the campaign back on the account goals"
}
}
}| searchPartners |
| boolean |
| serve on Google search partner sites (target_search_network) |
| loginCustomerId | string | manager id if operating through an MCC |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"dryRun": {
"type": "boolean",
"description": "validate with Google, write nothing"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change a LIVE (ENABLED) campaign"
},
"display": {
"type": "boolean",
"description": "serve on the Google Display Network (target_content_network)"
},
"campaignId": {
"type": "string",
"description": "the campaign to change"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"googleSearch": {
"type": "boolean",
"description": "serve on Google Search (target_google_search)"
},
"searchPartners": {
"type": "boolean",
"description": "serve on Google search partner sites (target_search_network)"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}| campaignId* | string | the campaign |
| customerId | string | omit to use the selected default account |
| scheduleMode | string | replace (default) or add to the current schedule |
| clearSchedule | boolean | remove the schedule so it runs all day |
| adGroupDevices | object | ad-group device multipliers (needs adGroupId) |
| loginCustomerId | string | manager id if operating through an MCC |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"dryRun": {
"type": "boolean",
"description": "validate with Google, change nothing"
},
"confirm": {
"type": "boolean",
"description": "true only after the user approved a change to a LIVE campaign or a delete"
},
"devices": {
"type": "object",
"properties": {
"mobile": {
"type": "number",
"description": "campaign bid multiplier for mobile; 1.2 = +20%, 0 = never show on mobile"
},
"tablet": {
"type": "number",
"description": "campaign bid multiplier for tablets; 1.2 = +20%, 0 = never show on tablets"
},
"desktop": {
"type": "number",
"description": "campaign bid multiplier for desktop; 1.2 = +20%, 0 = never show on desktop"
},
"connectedTv": {
"type": "number",
"description": "campaign bid multiplier for connected TV; 1.2 = +20%, 0 = never show there"
}
},
"description": "campaign-level device multipliers"
},
"schedule": {
"type": "array",
"items": {
"type": "object",
"required": [
"day",
"start",
"end"
],
"properties": {
"day": {
"type": "string",
"description": "MONDAY to SUNDAY, WEEKDAYS, WEEKENDS or EVERYDAY"
},
"end": {
"type": "string",
"description": "end time HH:MM in the account time zone; 24:00 allowed"
},
"start": {
"type": "string",
"description": "start time HH:MM in the account time zone, quarter hours, e.g. 09:00"
},
"bidModifier": {
"type": "number",
"description": "bid multiplier for this slot; 1.2 = +20%, 0.8 = -20%"
}
}
},
"description": "ad schedule slots; replaces the current schedule unless scheduleMode is add"
},
"adGroupId": {
"type": "string",
"description": "for adGroupDevices"
},
"locations": {
"type": "array",
"items": {
"type": "object",
"required": [
"geoTargetId",
"bidModifier"
],
"properties": {
"bidModifier": {
"type": "number",
"description": "bid multiplier for this location; 1.2 = +20%, 0.8 = -20%"
},
"geoTargetId": {
"type": "string",
"description": "Google geo target id, from find_google_ads_locations"
}
}
},
"description": "bid adjustments for locations the campaign already targets"
},
"campaignId": {
"type": "string",
"description": "the campaign"
},
"customerId": {
"type": "string",
"description": "omit to use the selected default account"
},
"scheduleMode": {
"enum": [
"replace",
"add"
],
"type": "string",
"description": "replace (default) or add to the current schedule"
},
"clearSchedule": {
"type": "boolean",
"description": "remove the schedule so it runs all day"
},
"adGroupDevices": {
"type": "object",
"properties": {
"mobile": {
"type": "number",
"description": "ad group bid multiplier for mobile; needs adGroupId; 0 = never show"
},
"tablet": {
"type": "number",
"description": "ad group bid multiplier for tablets; needs adGroupId; 0 = never show"
},
"desktop": {
"type": "number",
"description": "ad group bid multiplier for desktop; needs adGroupId; 0 = never show"
},
"connectedTv": {
"type": "number",
"description": "ad group bid multiplier for connected TV; needs adGroupId; 0 = never show"
}
},
"description": "ad-group device multipliers (needs adGroupId)"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
}
}
}| customerId |
| string |
| 10-digit account id (dashes ok) — omit to use the brand’s selected default 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 |
| loginCustomerId | string | manager id if operating through an MCC |
| campaignResourceName | string | full resource name, e.g. customers/{cid}/campaigns/{id} |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"status"
],
"properties": {
"adId": {
"type": "string",
"description": "ad id (level:\"ad\" — pass adGroupId too)"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "what to change — default campaign"
},
"status": {
"enum": [
"ENABLED",
"PAUSED",
"REMOVED"
],
"type": "string",
"description": "ENABLED = start spending; PAUSED = stop; REMOVED = permanent"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to ENABLE (real spend) or to REMOVE (permanent)"
},
"adGroupId": {
"type": "string",
"description": "ad group id (level:\"adGroup\", or with adId for level:\"ad\")"
},
"campaignId": {
"type": "string",
"description": "campaign id (level:\"campaign\")"
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"confirmName": {
"type": "string",
"description": "when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live"
},
"confirmChildren": {
"type": "number",
"description": "when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"campaignResourceName": {
"type": "string",
"description": "full resource name, e.g. customers/{cid}/campaigns/{id}"
}
}
}| customerId |
| string |
| omit to use the brand’s selected default account |
| countryCode | string | 2-letter hint to disambiguate a city name, e.g. CA for "London" |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
| excludedLocations | array | location NAMES to exclude |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and change nothing"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change a LIVE (ENABLED) campaign"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO language codes, e.g. [\"en\",\"es\"]"
},
"locations": {
"type": "array",
"items": {
"type": "string"
},
"description": "location NAMES to target"
},
"campaignId": {
"type": "string",
"description": "the campaign to target"
},
"customerId": {
"type": "string",
"description": "omit to use the brand’s selected default account"
},
"countryCode": {
"type": "string",
"description": "2-letter hint to disambiguate a city name, e.g. CA for \"London\""
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
},
"excludedLocations": {
"type": "array",
"items": {
"type": "string"
},
"description": "location NAMES to exclude"
}
}
}| true = apply the standard UTMs (utm_source, utm_medium, utm_campaign, utm_id, utm_content) |
| finalUrlSuffix | string | UTM string appended to every click, e.g. utm_source=google&utm_medium=cpc; empty string clears it |
| loginCustomerId | string | manager (MCC) account id when acting through a manager account; omit otherwise |
| 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} |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad to set tracking on; use with level ad"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "follows the id you pass"
},
"dryRun": {
"type": "boolean",
"description": "true = validate with Google and change nothing"
},
"confirm": {
"type": "boolean",
"description": "needed to change the tracking redirect on a LIVE campaign"
},
"adGroupId": {
"type": "string",
"description": "ad group to set tracking on; use with level adGroup"
},
"campaignId": {
"type": "string",
"description": "campaign to set tracking on; use with level campaign"
},
"customerId": {
"type": "string",
"description": "10-digit account id; omit to use the brand's selected default account"
},
"utmDefault": {
"type": "boolean",
"description": "true = apply the standard UTMs (utm_source, utm_medium, utm_campaign, utm_id, utm_content)"
},
"finalUrlSuffix": {
"type": "string",
"description": "UTM string appended to every click, e.g. utm_source=google&utm_medium=cpc; empty string clears it"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id when acting through a manager account; omit otherwise"
},
"trackingUrlTemplate": {
"type": "string",
"description": "click-tracking redirect template, e.g. {lpurl}?src=x; empty string clears it"
},
"urlCustomParameters": {
"type": "object",
"description": "up to 8 custom URL parameters as a key/value object, used in URLs as {_key}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
}
}
}| campaignGroupId | string | campaign group id when level is campaignGroup |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"status"
],
"properties": {
"level": {
"enum": [
"campaign",
"campaignGroup",
"creative"
],
"type": "string",
"description": "default campaign"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED",
"DRAFT"
],
"type": "string",
"description": "ACTIVE starts spend (needs confirm); PAUSED stops it; ARCHIVED is permanent"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to set ACTIVE (real spend)"
},
"campaignId": {
"type": "string",
"description": "campaign id when level is campaign"
},
"creativeId": {
"type": "string",
"description": "creative (ad) id when level is creative"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id from list_linkedin_ads_campaigns"
},
"campaignGroupId": {
"type": "string",
"description": "campaign group id when level is campaignGroup"
}
}
}| adAccountId | string | LinkedIn ad account id shared with this profile; needed only when several are shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"clear": {
"type": "boolean",
"description": "true removes all dynamic tracking parameters"
},
"level": {
"enum": [
"campaign",
"account"
],
"type": "string",
"description": "default campaign when campaignId is given"
},
"custom": {
"type": "object",
"description": "UTM key mapped to a fixed value, e.g. {utm_source: newsletter}; a new set replaces the old",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"dynamic": {
"type": "object",
"description": "UTM key mapped to a LinkedIn macro, e.g. {utm_campaign: CAMPAIGN_NAME}; a new set replaces the old",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"standard": {
"type": "boolean",
"description": "true sets the standard UTM set: source linkedin, medium paid_social, campaign and content filled per click"
},
"campaignId": {
"type": "string",
"description": "campaign to set tracking on; omit for account-level tracking with level account"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
}
}
}| number | null |
| local only. null clears it |
| feedLabel* | string | usually the feed target country, e.g. "US" |
| pickupSla | string | null | local only |
| salePrice | string | null | "9.99 USD". Google requires salePriceEffectiveDate alongside it if that is set. null clears it |
| storeCode | string | required for kind:'local' — the store ID from the merchant's Business Profile. Immutable |
| availability | string | null | no API enum exists; Google's local spec publishes In stock, Limited availability, On display to order, Out of stock, in English. Anything else is warned about, not refused. null clears it |
| pickupMethod | string | null | local only. Must be sent WITH pickupSla unless the value is "not supported" |
| contentLanguage* | string | two-letter feed language, e.g. "en" |
| merchantCenterId* | string | from list_merchant_accounts |
| instoreProductLocation | string | null | local only, max 20 bytes |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"offerId",
"contentLanguage",
"feedLabel",
"kind"
],
"properties": {
"kind": {
"enum": [
"local",
"regional"
],
"type": "string",
"description": "local = one physical store (storeCode), regional = one Google region (region)"
},
"price": {
"type": [
"string",
"null"
],
"description": "\"12.99 USD\". A bare number is refused because a price with no currency is not a price. null clears it"
},
"region": {
"type": "string",
"description": "required for kind:'regional' — the Merchant Center region id. Immutable"
},
"offerId": {
"type": "string",
"description": "the product's own id in the feed"
},
"quantity": {
"type": [
"number",
"null"
],
"description": "local only. null clears it"
},
"feedLabel": {
"type": "string",
"description": "usually the feed target country, e.g. \"US\""
},
"pickupSla": {
"type": [
"string",
"null"
],
"description": "local only"
},
"salePrice": {
"type": [
"string",
"null"
],
"description": "\"9.99 USD\". Google requires salePriceEffectiveDate alongside it if that is set. null clears it"
},
"storeCode": {
"type": "string",
"description": "required for kind:'local' — the store ID from the merchant's Business Profile. Immutable"
},
"availability": {
"type": [
"string",
"null"
],
"description": "no API enum exists; Google's local spec publishes In stock, Limited availability, On display to order, Out of stock, in English. Anything else is warned about, not refused. null clears it"
},
"pickupMethod": {
"type": [
"string",
"null"
],
"description": "local only. Must be sent WITH pickupSla unless the value is \"not supported\""
},
"contentLanguage": {
"type": "string",
"description": "two-letter feed language, e.g. \"en\""
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
},
"instoreProductLocation": {
"type": [
"string",
"null"
],
"description": "local only, max 20 bytes"
}
}
}| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"properties"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "the user said yes. Required for ProfileExpansionEnabled=true, AllowImageAutoRetrieve=true and any AIDetectedContentWatermarkPolicy value"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"properties": {
"type": "object",
"description": "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>\"}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| criterionIds | array | criterion ids to remove (action remove), from list_microsoft_ads_audience_targeting |
| bidAdjustment | number | -90 to 900 (%); 0 = target without changing the bid |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"action": {
"enum": [
"add",
"remove"
],
"type": "string",
"description": "default add"
},
"confirm": {
"type": "boolean",
"description": "true required on a LIVE campaign when targeting or removing"
},
"exclude": {
"type": "boolean",
"description": "true = exclude this audience instead of targeting it"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "ad group to target or exclude the audience on; pass this or campaignId"
},
"audienceId": {
"type": "string",
"description": "list_microsoft_ads_audiences"
},
"campaignId": {
"type": "string",
"description": "campaign to target or exclude the audience on; pass this or adGroupId"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"criterionIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "criterion ids to remove (action remove), from list_microsoft_ads_audience_targeting"
},
"bidAdjustment": {
"type": "number",
"description": "-90 to 900 (%); 0 = target without changing the bid"
}
}
}| accountId | string | older name for adAccountId (MCP only) |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
| confirmTypes | array | 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"optIns"
],
"properties": {
"optIns": {
"type": "object",
"description": "map of Microsoft auto-apply type name to true/false, e.g. {\"RemoveConflictingNegativeKeywordOpportunity\": false}. Case-sensitive keys: ResponsiveSearchAdsOpportunity, MultiMediaAdsOpportunity, RemoveConflictingNegativeKeywordOpportunity, FixConversionGoalSettingsOpportunity, CreateConversionGoalOpportunity",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "boolean"
}
},
"confirm": {
"type": "boolean",
"description": "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"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"confirmTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "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"
}
}
}| string |
| Microsoft ad account id — omit to use the single shared account |
| biddingScheme* | string | new bid strategy; TargetCpa and TargetRoas are sent as MaxConversions and MaxConversionValue |
| targetAdPosition | string | FirstPage, MainLine or MainLine1 (MainLine1 = very top), with TargetImpressionShare |
| targetImpressionShare | number | 1–100 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId",
"biddingScheme"
],
"properties": {
"maxCpc": {
"type": "number",
"description": "bid cap in account currency, for MaxClicks and MaxConversions"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true on a LIVE campaign"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"targetCpa": {
"type": "number",
"description": "target cost per conversion in account currency, for MaxConversions"
},
"campaignId": {
"type": "string",
"description": "Microsoft campaign id whose bid strategy changes"
},
"targetRoas": {
"type": "number",
"description": "ratio, e.g. 4 = 400%"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
},
"biddingScheme": {
"enum": [
"EnhancedCpc",
"MaxClicks",
"MaxConversions",
"MaxConversionValue",
"TargetImpressionShare",
"TargetCpa",
"TargetRoas"
],
"type": "string",
"description": "new bid strategy; TargetCpa and TargetRoas are sent as MaxConversions and MaxConversionValue"
},
"targetAdPosition": {
"enum": [
"FirstPage",
"MainLine",
"MainLine1"
],
"type": "string",
"description": "FirstPage, MainLine or MainLine1 (MainLine1 = very top), with TargetImpressionShare"
},
"targetImpressionShare": {
"type": "number",
"description": "1–100"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"add": {
"type": "array",
"items": {
"type": "object",
"required": [
"profileType",
"profileId"
],
"properties": {
"profileId": {
"type": "string",
"description": "profile id from search_microsoft_ads_profiles; job title and seniority ids are passed as-is"
},
"profileType": {
"enum": [
"CompanyName",
"Industry",
"JobFunction",
"JobSeniority",
"JobTitle"
],
"type": "string",
"description": "type of the profileId: CompanyName, Industry, JobFunction, JobSeniority or JobTitle"
},
"bidAdjustment": {
"type": "number",
"description": "whole percent, -90 to 900; omit for none"
}
}
},
"description": "profile criteria to add: [{profileType, profileId, bidAdjustment}]"
},
"update": {
"type": "array",
"items": {
"type": "object",
"required": [
"criterionId",
"bidAdjustment"
],
"properties": {
"criterionId": {
"type": "string",
"description": "existing criterion id from list_microsoft_ads_profile_targeting"
},
"bidAdjustment": {
"type": "number",
"description": "whole percent, -90 to 900"
}
}
},
"description": "bid adjustments to change on existing criteria: [{criterionId, bidAdjustment}]"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true when the campaign is ACTIVE"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "Microsoft campaign id to change LinkedIn profile targeting on"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}| campaignId |
| string |
| campaign id (level:"campaign") |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"status"
],
"properties": {
"adId": {
"type": "string",
"description": "ad id (level:\"ad\" — pass adGroupId too)"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "what to change — default campaign"
},
"status": {
"enum": [
"Active",
"Paused"
],
"type": "string",
"description": "Active = start spending; Paused = stop"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to set Active (real spend)"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"adGroupId": {
"type": "string",
"description": "ad group id (level:\"adGroup\", or with adId for level:\"ad\")"
},
"campaignId": {
"type": "string",
"description": "campaign id (level:\"campaign\")"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}| boolean |
| true adds another non-overlapping daily rule instead of editing one |
| windowId | string | the daily rule to change, from list_openai_ads_spend_windows; required when the account has more than one |
| noEndDate | boolean | true removes the end date |
| startDate | string | YYYY-MM-DD, first day of a NEW daily limit (tomorrow or later) |
| expectedRevision | number | the revision from list_openai_ads_spend_windows |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"amount"
],
"properties": {
"amount": {
"type": "number",
"description": "daily limit in the account currency; 0 stops all account spend"
},
"confirm": {
"type": "boolean",
"description": "true applies the change; needs expectedRevision as well"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD, exclusive"
},
"newRule": {
"type": "boolean",
"description": "true adds another non-overlapping daily rule instead of editing one"
},
"windowId": {
"type": "string",
"description": "the daily rule to change, from list_openai_ads_spend_windows; required when the account has more than one"
},
"noEndDate": {
"type": "boolean",
"description": "true removes the end date"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD, first day of a NEW daily limit (tomorrow or later)"
},
"expectedRevision": {
"type": "number",
"description": "the revision from list_openai_ads_spend_windows"
}
}
}| string |
| YYYY-MM-DD, first day of the window (inclusive) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ioId": {
"type": "string",
"description": "insertion-order id, when billing uses one"
},
"name": {
"type": "string",
"description": "window name"
},
"amount": {
"type": "number",
"description": "spend ceiling for the whole account over the window, in the account currency"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD, end of the window (exclusive; the window ends the day before)"
},
"windowId": {
"type": "string",
"description": "spend-limit window id to edit; omit to create a new window"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD, first day of the window (inclusive)"
}
}
}| REQUIRED true to activate (real spend), to archive (irreversible), or for EITHER direction at level "account" |
| adGroupId | string | ad group id to change; use with level adGroup |
| campaignId | string | campaign id to change; use with level campaign |
| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"status"
],
"properties": {
"adId": {
"type": "string",
"description": "ad id to change; use with level ad"
},
"level": {
"enum": [
"account",
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "inferred from which id you pass — default campaign. \"account\" is the whole ad account and takes no id."
},
"status": {
"enum": [
"active",
"paused",
"archived"
],
"type": "string",
"description": "active = start spending; paused = stop; archived = permanent (not available at level \"account\")"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to activate (real spend), to archive (irreversible), or for EITHER direction at level \"account\""
},
"adGroupId": {
"type": "string",
"description": "ad group id to change; use with level adGroup"
},
"campaignId": {
"type": "string",
"description": "campaign id to change; use with level campaign"
},
"confirmName": {
"type": "string",
"description": "when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live"
},
"confirmChildren": {
"type": "number",
"description": "when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any"
}
}
}| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"status"
],
"properties": {
"adId": {
"type": "string",
"description": "ad id to change; use with level ad"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "what to change — default campaign"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED",
"DRAFT"
],
"type": "string",
"description": "ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire (Pinterest’s delete)"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true for ACTIVE (real spend) or ARCHIVED (irreversible retirement)"
},
"adGroupId": {
"type": "string",
"description": "ad group id to change; use with level adGroup"
},
"campaignId": {
"type": "string",
"description": "campaign id to change; use with level campaign"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id, from list_pinterest_ads_campaigns"
},
"confirmName": {
"type": "string",
"description": "when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live"
},
"confirmChildren": {
"type": "number",
"description": "when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any"
}
}
}| 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 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"kind",
"id",
"status"
],
"properties": {
"id": {
"type": "string",
"description": "id of the campaign, ad group or ad"
},
"kind": {
"enum": [
"campaign",
"ad_group",
"ad"
],
"type": "string",
"description": "campaign, ad_group or ad: which type the id refers to"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED",
"DELETED"
],
"type": "string",
"description": "ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire; DELETED = permanent, and blocked for 3h after any change"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true for ACTIVE (real spend), ARCHIVED and DELETED"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"confirmName": {
"type": "string",
"description": "when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live"
},
"confirmChildren": {
"type": "number",
"description": "when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any"
}
}
}| ONLY if already multiplied by 1,000,000; never alongside dailyBudget |
| lifetimeBudgetMicro | number | lifetime budget in micro-currency; only if already converted, never with lifetimeBudget |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the ad squad id"
},
"level": {
"enum": [
"adsquad"
],
"type": "string",
"description": "adsquad — the only tier that holds a budget on Snapchat"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — without it nothing changes and you get the sentence to show the user"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"dailyBudget": {
"type": "number",
"description": "in the ad account’s own currency — minimum 5"
},
"lifetimeBudget": {
"type": "number",
"description": "lifetime budget in the ad account's currency; an alternative to dailyBudget"
},
"dailyBudgetMicro": {
"type": "number",
"description": "ONLY if already multiplied by 1,000,000; never alongside dailyBudget"
},
"lifetimeBudgetMicro": {
"type": "number",
"description": "lifetime budget in micro-currency; only if already converted, never with lifetimeBudget"
}
}
}| coverAllObjectives* | boolean | REQUIRED by TikTok on every update. true is PERMANENT — it cannot be set back to false. |
| categoryExclusionIds | array | from get_tiktok_ads_brand_safety’s availableCategoryExclusions. Send [] explicitly to have none, so clearing them is something you said rather than something that happened. |
| verticalSensitivityId | string | from get_tiktok_ads_brand_safety’s availableVerticalSensitivity |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"coverAllObjectives"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true only to turn coverAllObjectives ON for the first time, because that is irreversible"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
},
"brandSafetyType": {
"enum": [
"EXPANDED_INVENTORY",
"STANDARD_INVENTORY",
"LIMITED_INVENTORY"
],
"type": "string",
"description": "required whenever you change any suitability control"
},
"coverAllObjectives": {
"type": "boolean",
"description": "REQUIRED by TikTok on every update. true is PERMANENT — it cannot be set back to false."
},
"categoryExclusionIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "from get_tiktok_ads_brand_safety’s availableCategoryExclusions. Send [] explicitly to have none, so clearing them is something you said rather than something that happened."
},
"verticalSensitivityId": {
"type": "string",
"description": "from get_tiktok_ads_brand_safety’s availableVerticalSensitivity"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id",
"budget"
],
"properties": {
"id": {
"type": "string",
"description": "the campaign or ad group id"
},
"level": {
"enum": [
"campaign",
"adgroup"
],
"type": "string",
"description": "which tier holds the budget — default campaign"
},
"budget": {
"type": "number",
"description": "the new budget in the advertiser’s own currency — must be above zero"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — call without it first to see exactly what would change"
},
"budgetMode": {
"type": "string",
"description": "default BUDGET_MODE_DAY"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}| advertiserId |
| string |
| TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| confirmScope | string | REQUIRED as well, to arm a rule that can start or raise spend: the scopeToken from list_tiktok_ads_rules. The refusal prints the value it wants. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"ruleIds",
"operateType"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "REQUIRED true for every operation — call without it first to see what would happen"
},
"ruleIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the rules to change"
},
"operateType": {
"enum": [
"TURN_ON",
"TURN_OFF",
"DELETE"
],
"type": "string",
"description": "TURN_ON = start running it unattended; TURN_OFF = stop, reversible; DELETE = remove, NOT reversible"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"confirmScope": {
"type": "string",
"description": "REQUIRED as well, to arm a rule that can start or raise spend: the scopeToken from list_tiktok_ads_rules. The refusal prints the value it wants."
}
}
}| advertiserId |
| string |
| TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"ids",
"status"
],
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
},
"description": "the campaign / ad group / ad ids to change"
},
"level": {
"enum": [
"campaign",
"adgroup",
"ad"
],
"type": "string",
"description": "which tier the ids belong to"
},
"status": {
"enum": [
"ENABLE",
"DISABLE",
"DELETE"
],
"type": "string",
"description": "ENABLE = start real spend; DISABLE = stop; DELETE = remove (TikTok has no delete verb, removal is a status)"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — call without it first to see exactly what would change"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"itemId",
"enabled"
],
"properties": {
"itemId": {
"type": "string",
"description": "the TikTok post id"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true when enabling — it makes the post publicly promotable and accepts TikTok’s advertising terms on the owner’s behalf"
},
"enabled": {
"type": "boolean",
"description": "true turns Ad authorization ON (and mints the Spark Ads code); false turns it off"
},
"authorizationDays": {
"type": "number",
"description": "7 | 30 | 60 | 180 | 365 — TikTok publishes exactly those five; default 30"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"status"
],
"properties": {
"status": {
"enum": [
"ACTIVE",
"PAUSED"
],
"type": "string",
"description": "ACTIVE starts real spend; PAUSED stops delivery"
},
"confirm": {
"type": "boolean",
"description": "required to set ACTIVE — real money"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "the whole campaign — pass this OR lineItemId"
},
"lineItemId": {
"type": "string",
"description": "one ad group — pass this OR campaignId"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"fileId"
],
"properties": {
"type": {
"enum": [
"view",
"edit",
"embed"
],
"type": "string",
"description": "view (default), edit, or embed (personal OneDrive only)"
},
"scope": {
"enum": [
"anonymous",
"organization",
"users"
],
"type": "string",
"description": "anonymous (default, anyone with the link), organization (work or school), or users (invited people only)"
},
"fileId": {
"type": "string",
"description": "OneDrive file or folder id from list_onedrive_files"
},
"confirm": {
"type": "boolean",
"description": "required for an anonymous edit link"
},
"password": {
"type": "string",
"description": "personal OneDrive with Microsoft 365"
},
"expiresAt": {
"type": "string",
"description": "e.g. 2026-11-30 or 2026-11-30T17:00:00Z"
}
}
}| startTime | string | ISO 8601, on the start of an hour (22:00, never 22:45) |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| asyncFormat | string | csv or excel; file format for an async:true report |
| granularity | string | default TOTAL. DAY and HOUR need startTime and endTime, both on the hour. |
| reportRunId | string | fetch an async report started earlier — same level and id |
| reportDimension | string | break the numbers down by AUDIENCE — age, gender, age,gender, country, region, dma, os, device make or interest (lifestyle_category). Not with HOUR. region / dma / make / lifestyle_category / the os+country pairs carry DELIVERY metrics only (no conversion_* fields) |
| actionReportTime | string | count a conversion on the day it happened (conversion, default) or on the day of the ad impression (impression) |
| conversionSourceTypes | string | comma-separated: web, app, offline, total, total_off_platform, total_on_platform — splits conversions by where they happened |
| viewAttributionWindow | string | how long after a VIEW (no swipe) a conversion still counts; 'none' attributes no view-throughs at all. Omit to use the ad account default. |
| swipeUpAttributionWindow | string | how long after a SWIPE a conversion still counts in this report. Omit to use the ad account default. It only changes how this report counts conversions; the ad account itself is not changed. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
"type": "string",
"description": "the object to report on — defaults to the ad account itself for level:adaccount"
},
"async": {
"type": "boolean",
"description": "for long ranges or wide breakdowns: starts a report run on Snapchat that builds a downloadable CSV/Excel file (kept seven days) and returns its reportRunId"
},
"level": {
"enum": [
"adaccount",
"campaign",
"adsquad",
"ad"
],
"type": "string",
"description": "default campaign"
},
"fields": {
"type": "string",
"description": "comma-separated metrics — default impressions,spend,swipes"
},
"endTime": {
"type": "string",
"description": "ISO 8601, on the start of an hour"
},
"breakdown": {
"type": "string",
"description": "object-level breakdown: ad, adsquad (campaign stats only) or campaign (ad-account stats only)"
},
"omitEmpty": {
"type": "boolean",
"description": "drop days/entities with zero on every metric"
},
"startTime": {
"type": "string",
"description": "ISO 8601, on the start of an hour (22:00, never 22:45)"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"asyncFormat": {
"enum": [
"csv",
"excel"
],
"type": "string",
"description": "csv or excel; file format for an async:true report"
},
"granularity": {
"enum": [
"TOTAL",
"DAY",
"HOUR",
"LIFETIME"
],
"type": "string",
"description": "default TOTAL. DAY and HOUR need startTime and endTime, both on the hour."
},
"reportRunId": {
"type": "string",
"description": "fetch an async report started earlier — same level and id"
},
"reportDimension": {
"enum": [
"country",
"country,os",
"region",
"dma",
"gender",
"age",
"age,gender",
"os",
"os,country",
"make",
"lifestyle_category"
],
"type": "string",
"description": "break the numbers down by AUDIENCE — age, gender, age,gender, country, region, dma, os, device make or interest (lifestyle_category). Not with HOUR. region / dma / make / lifestyle_category / the os+country pairs carry DELIVERY metrics only (no conversion_* fields)"
},
"actionReportTime": {
"enum": [
"conversion",
"impression"
],
"type": "string",
"description": "count a conversion on the day it happened (conversion, default) or on the day of the ad impression (impression)"
},
"conversionSourceTypes": {
"type": "string",
"description": "comma-separated: web, app, offline, total, total_off_platform, total_on_platform — splits conversions by where they happened"
},
"viewAttributionWindow": {
"enum": [
"none",
"1_HOUR",
"3_HOUR",
"6_HOUR",
"1_DAY",
"7_DAY"
],
"type": "string",
"description": "how long after a VIEW (no swipe) a conversion still counts; 'none' attributes no view-throughs at all. Omit to use the ad account default."
},
"swipeUpAttributionWindow": {
"enum": [
"1_DAY",
"7_DAY",
"28_DAY"
],
"type": "string",
"description": "how long after a SWIPE a conversion still counts in this report. Omit to use the ad account default. It only changes how this report counts conversions; the ad account itself is not changed."
}
}
}| string |
| default text/html |
| dynamicServing | string | none (default) | pc-laptop | mobile | amp | tablet | non-visual-browser |
| structuredData | string | optional JSON-LD, for non-HTML content types |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"siteUrl",
"url",
"html"
],
"properties": {
"url": {
"type": "string",
"description": "the full URL on this site that this content belongs to"
},
"html": {
"type": "string",
"description": "the page content Bing should index for that URL"
},
"status": {
"type": "number",
"description": "HTTP status for the message, default 200"
},
"headers": {
"type": "object",
"description": "extra response headers. Content-Length is always computed and never taken from you.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"siteUrl": {
"type": "string",
"description": "the exact site string from list_bing_webmaster_sites"
},
"contentType": {
"type": "string",
"description": "default text/html"
},
"dynamicServing": {
"type": "string",
"description": "none (default) | pc-laptop | mobile | amp | tablet | non-visual-browser"
},
"structuredData": {
"type": "string",
"description": "optional JSON-LD, for non-HTML content types"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"host",
"key",
"urls"
],
"properties": {
"key": {
"type": "string",
"description": "the IndexNow key — 8-128 letters, numbers and dashes — ALREADY hosted as <key>.txt on that site"
},
"host": {
"type": "string",
"description": "the bare hostname that owns these URLs, e.g. \"example.com\""
},
"urls": {
"type": "array",
"items": {
"type": "string"
},
"description": "the full URLs that were added, updated or deleted, all on `host`"
},
"keyLocation": {
"type": "string",
"description": "the full URL of the key file if it is NOT at the site root. Moving it NARROWS the key to that directory only."
}
}
}| advertiserId | string | pass this OR bcId, not both |
| fileTypeCode | string | from list_tiktok_ads_verification_documents |
| industryCode | string | BUSINESS only |
| regionIsoCode | string | the account’s ISO country |
| individualName | string | INDIVIDUAL: the full legal name, PERMANENT |
| verificationType* | string | BUSINESS (a company) or INDIVIDUAL (a person) |
| qualificationImageIds | array | exactly 1 for BUSINESS, exactly 2 for INDIVIDUAL (front and back) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"verificationType"
],
"properties": {
"bcId": {
"type": "string",
"description": "Business Center id, for a Business Center verification; use instead of advertiserId"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true: call without it first to see exactly what would be sent"
},
"licenseNo": {
"type": "string",
"description": "BUSINESS: the certificate number, PERMANENT"
},
"identityNo": {
"type": "string",
"description": "INDIVIDUAL: the government ID number, PERMANENT"
},
"websiteUrl": {
"type": "string",
"description": "required for both"
},
"companyName": {
"type": "string",
"description": "BUSINESS: the legal name, PERMANENT, must match the document exactly"
},
"advertiserId": {
"type": "string",
"description": "pass this OR bcId, not both"
},
"fileTypeCode": {
"type": "string",
"description": "from list_tiktok_ads_verification_documents"
},
"industryCode": {
"enum": [
"NOT_APPLICABLE",
"ALCOHOL",
"OTC",
"DATING_APP",
"FINANCIAL_SERVICES"
],
"type": "string",
"description": "BUSINESS only"
},
"regionIsoCode": {
"type": "string",
"description": "the account’s ISO country"
},
"individualName": {
"type": "string",
"description": "INDIVIDUAL: the full legal name, PERMANENT"
},
"verificationType": {
"enum": [
"BUSINESS",
"INDIVIDUAL"
],
"type": "string",
"description": "BUSINESS (a company) or INDIVIDUAL (a person)"
},
"qualificationImageIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "exactly 1 for BUSINESS, exactly 2 for INDIVIDUAL (front and back)"
}
}
}| string |
| PRODUCT_ATTRIBUTES, PRODUCT_REVIEW, CATALOG, PIXEL_OR_EVENT or FILE_UPLOAD_OR_FEED; omit for all |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"catalogId"
],
"properties": {
"bcId": {
"type": "string",
"description": "from list_tiktok_ads_business_centers"
},
"page": {
"type": "number",
"description": "page number, from 1"
},
"feedId": {
"type": "string",
"description": "defaults to ALL feeds in the catalog"
},
"pageSize": {
"type": "number",
"description": "up to 20"
},
"catalogId": {
"type": "string",
"description": "from list_tiktok_ads_catalogs"
},
"issueLevel": {
"enum": [
"CRITICAL",
"WARNING"
],
"type": "string",
"description": "CRITICAL or WARNING; omit for both"
},
"issueCategory": {
"enum": [
"PRODUCT_ATTRIBUTES",
"PRODUCT_REVIEW",
"CATALOG",
"PIXEL_OR_EVENT",
"FILE_UPLOAD_OR_FEED"
],
"type": "string",
"description": "PRODUCT_ATTRIBUTES, PRODUCT_REVIEW, CATALOG, PIXEL_OR_EVENT or FILE_UPLOAD_OR_FEED; omit for all"
}
}
}| number |
| rows per page, 1–1000 (default 100). Adding "stat_time_day" overflows a page fast: 30 days x 4 campaigns is already 120 rows. |
| filtering | array | TikTok’s own filter array, e.g. [{"field_name":"campaign_ids","filter_type":"IN","filter_value":"['123']"}]. Narrows the report AT TIKTOK rather than after truncation. |
| startDate | string | YYYY-MM-DD |
| dimensions | array | TikTok dimensions to group by — default the level’s own id, e.g. ["campaign_id"]. Add "stat_time_day" for a daily breakdown. On an AUDIENCE report these are audience attributes instead, e.g. ["age"]. |
| reportType | string | BASIC (default) = spend + performance. AUDIENCE = the audience breakdown; its dimensions are audience ATTRIBUTES (age, gender, country_code, interest_category …) and BASIC rejects those, so name them. PLAYABLE_MATERIAL = playable ads, CATALOG = Dynamic Showcase Ads. TikTok also documents BC (Business Center) and TT_SHOP (GMV max) — both are refused here by name with the reason. |
| advertiserId | string | from list_tiktok_ads_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "1-based page (default 1). A report LONGER than one page comes back with hasMore:true and a truncationNote — totals computed from one page are WRONG."
},
"level": {
"enum": [
"campaign",
"adgroup",
"ad",
"advertiser"
],
"type": "string",
"description": "the reporting level — default campaign. \"advertiser\" is the whole account, which is the level an audience breakdown is normally run at."
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"metrics": {
"type": "array",
"items": {
"type": "string"
},
"description": "TikTok metric names — default [\"spend\",\"impressions\",\"clicks\",\"ctr\",\"cpc\",\"cpm\",\"conversion\"]"
},
"pageSize": {
"type": "number",
"description": "rows per page, 1–1000 (default 100). Adding \"stat_time_day\" overflows a page fast: 30 days x 4 campaigns is already 120 rows."
},
"filtering": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "TikTok’s own filter array, e.g. [{\"field_name\":\"campaign_ids\",\"filter_type\":\"IN\",\"filter_value\":\"['123']\"}]. Narrows the report AT TIKTOK rather than after truncation."
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"dimensions": {
"type": "array",
"items": {
"type": "string"
},
"description": "TikTok dimensions to group by — default the level’s own id, e.g. [\"campaign_id\"]. Add \"stat_time_day\" for a daily breakdown. On an AUDIENCE report these are audience attributes instead, e.g. [\"age\"]."
},
"reportType": {
"enum": [
"BASIC",
"AUDIENCE",
"PLAYABLE_MATERIAL",
"CATALOG"
],
"type": "string",
"description": "BASIC (default) = spend + performance. AUDIENCE = the audience breakdown; its dimensions are audience ATTRIBUTES (age, gender, country_code, interest_category …) and BASIC rejects those, so name them. PLAYABLE_MATERIAL = playable ads, CATALOG = Dynamic Showcase Ads. TikTok also documents BC (Business Center) and TT_SHOP (GMV max) — both are refused here by name with the reason."
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts"
}
}
}| languages | array | language codes from search_tiktok_ads_targeting(kind:"language"); omit for all |
| locationIds* | array | REQUIRED, and from ONE country or region only — ids from search_tiktok_ads_targeting(kind:"location") |
| advertiserId | string | from list_tiktok_ads_accounts — omit only when exactly one is reachable |
| purchasedReach | number | IN THOUSANDS — required with FIXED_REACH |
| brandSafetyType | string | NO_BRAND_SAFETY (default) | EXPANDED_INVENTORY | STANDARD_INVENTORY | LIMITED_INVENTORY |
| rfPurchasedType | string | FIXED_BUDGET | FIXED_SHOW | FIXED_REACH — which one you FIX. Pass its own number too (budget / purchasedImpression / purchasedReach). Omit entirely for the unconstrained range. |
| scheduleEndTime* | string | "YYYY-MM-DD HH:59:59" UTC. On THIS endpoint the window may not exceed 30 days (the create allows 90). |
| operatingSystems | array | ANDROID, IOS, PC |
| frequencySchedule* | number | the cycle in days, at most min(campaign length, 30) |
| scheduleStartTime* | string | "YYYY-MM-DD HH:00:00" UTC. R&F is booked in whole days IN THE TARGET REGION’S time zone expressed as UTC, so an advertiser in UTC-5 starting Nov 3 passes "2020-11-03 05:00:00" — not midnight. |
| interestCategoryIds | array | ids from search_tiktok_ads_targeting(kind:"interest") |
| purchasedImpression | number | IN THOUSANDS — required with FIXED_SHOW |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"locationIds",
"scheduleStartTime",
"scheduleEndTime",
"frequency",
"frequencySchedule"
],
"properties": {
"budget": {
"type": "number",
"description": "required with FIXED_BUDGET"
},
"gender": {
"type": "string",
"description": "TikTok gender value; omit to reach everyone"
},
"feedType": {
"type": "string",
"description": "STANDARD_FEED (large inventory, normal CPM) or TOP_FEED (limited inventory, higher CPM)"
},
"ageGroups": {
"type": "array",
"items": {
"type": "string"
},
"description": "TikTok age band values; omit to reach all ages"
},
"frequency": {
"type": "number",
"description": "with frequencySchedule, the cap: frequency 2 + frequencySchedule 3 means \"at most twice every 3 days\". Must be <= frequencySchedule and never more than 4 per day."
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "language codes from search_tiktok_ads_targeting(kind:\"language\"); omit for all"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REQUIRED, and from ONE country or region only — ids from search_tiktok_ads_targeting(kind:\"location\")"
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit only when exactly one is reachable"
},
"purchasedReach": {
"type": "number",
"description": "IN THOUSANDS — required with FIXED_REACH"
},
"brandSafetyType": {
"type": "string",
"description": "NO_BRAND_SAFETY (default) | EXPANDED_INVENTORY | STANDARD_INVENTORY | LIMITED_INVENTORY"
},
"rfPurchasedType": {
"type": "string",
"description": "FIXED_BUDGET | FIXED_SHOW | FIXED_REACH — which one you FIX. Pass its own number too (budget / purchasedImpression / purchasedReach). Omit entirely for the unconstrained range."
},
"scheduleEndTime": {
"type": "string",
"description": "\"YYYY-MM-DD HH:59:59\" UTC. On THIS endpoint the window may not exceed 30 days (the create allows 90)."
},
"operatingSystems": {
"type": "array",
"items": {
"type": "string"
},
"description": "ANDROID, IOS, PC"
},
"frequencySchedule": {
"type": "number",
"description": "the cycle in days, at most min(campaign length, 30)"
},
"scheduleStartTime": {
"type": "string",
"description": "\"YYYY-MM-DD HH:00:00\" UTC. R&F is booked in whole days IN THE TARGET REGION’S time zone expressed as UTC, so an advertiser in UTC-5 starting Nov 3 passes \"2020-11-03 05:00:00\" — not midnight."
},
"interestCategoryIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ids from search_tiktok_ads_targeting(kind:\"interest\")"
},
"purchasedImpression": {
"type": "number",
"description": "IN THOUSANDS — required with FIXED_SHOW"
}
}
}| number |
| rows per page |
| ruleInfo | array | rule ids or names |
| dimension | string | level the rule watches: CAMPAIGN, ADGROUP or AD |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"time": {
"type": "array",
"items": {
"type": "string"
},
"description": "exactly two UTC datetimes: start and end"
},
"action": {
"enum": [
"TURN_ON",
"TURN_OFF",
"MESSAGE",
"DAILY_BUDGET",
"LIFETIME_BUDGET",
"BID"
],
"type": "string",
"description": "filter to one kind of action — use this to answer \"has anything raised my budgets?\""
},
"execId": {
"type": "string",
"description": "for the DETAIL of one run — must be sent with ruleId from the same row"
},
"ruleId": {
"type": "string",
"description": "for the DETAIL of one run — must be sent with execId from the same row"
},
"status": {
"enum": [
"ON",
"OFF",
"DELETED"
],
"type": "string",
"description": "ON, OFF or DELETED; status of the rules whose runs are listed"
},
"pageSize": {
"type": "number",
"description": "rows per page"
},
"ruleInfo": {
"type": "array",
"items": {
"type": "string"
},
"description": "rule ids or names"
},
"dimension": {
"enum": [
"CAMPAIGN",
"ADGROUP",
"AD"
],
"type": "string",
"description": "level the rule watches: CAMPAIGN, ADGROUP or AD"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ad ids to read (AD report)"
},
"videoId": {
"type": "string",
"description": "VIDEO report: one video id from upload_tiktok_ads_creative"
},
"adgroupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ad group ids to read (AD report)"
},
"materialId": {
"type": "string",
"description": "creative material id to read one specific material"
},
"reportType": {
"enum": [
"AD",
"VIDEO"
],
"type": "string",
"description": "AD (default) reads ads by adIds, adgroupIds or campaignIds; VIDEO reads one video by videoId"
},
"campaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "campaign ids to read (AD report)"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"ids"
],
"properties": {
"ids": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Smart+ ids from list_tiktok_smart_campaigns"
},
"level": {
"enum": [
"CAMPAIGN",
"ADGROUP"
],
"type": "string",
"description": "which Smart+ level the ids are"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD, on or after startDate and within 60 days of it"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD in the ad account timezone. With endDate this returns HISTORY instead of status; must be within the last 60 days"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
}
}
}| string |
| BRANDED_CONTENT needs an industry label, ORGANIC_CONTENT a content label |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"rankingType",
"timePeriod",
"lookback",
"labelId"
],
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"labelId": {
"type": "string",
"description": "from list_tiktok_creator_labels(labelType:\"RANKING\"), matching rankingType"
},
"lookback": {
"enum": [
"ONE",
"TWO",
"THREE"
],
"type": "string",
"description": "ONE is the most recent completed period"
},
"pageSize": {
"type": "number",
"description": "1-100, default 20"
},
"timePeriod": {
"enum": [
"WEEK",
"MONTH"
],
"type": "string",
"description": "ranking period: WEEK or MONTH"
},
"countryCode": {
"type": "string",
"description": "US: the only country TikTok ranks"
},
"rankingType": {
"enum": [
"BRANDED_CONTENT",
"ORGANIC_CONTENT"
],
"type": "string",
"description": "BRANDED_CONTENT needs an industry label, ORGANIC_CONTENT a content label"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"kind"
],
"properties": {
"kind": {
"enum": [
"sounds",
"sound_videos",
"search_terms",
"search_suggestions"
],
"type": "string",
"description": "sounds (trending tracks), sound_videos (videos using soundId), search_terms, or search_suggestions (needs query)"
},
"genre": {
"type": "string",
"description": "music genre for kind sounds, e.g. POP or HIP_HOP/RAP, or ALL"
},
"query": {
"type": "string",
"description": "for search_suggestions"
},
"soundId": {
"type": "string",
"description": "for sound_videos — soundId from kind sounds"
},
"dateRange": {
"enum": [
"1DAY",
"7DAY",
"30DAY",
"90DAY"
],
"type": "string",
"description": "trend window: 1DAY, 7DAY, 30DAY or 90DAY"
},
"countryCode": {
"type": "string",
"description": "2-letter country code the trend data is for, e.g. US"
},
"personalized": {
"type": "boolean",
"description": "search terms personalized for this account"
}
}
}| countryCode |
| string |
| one of the campaign’s own country codes: resolved automatically when it has only one |
| campaignType | string | CAMPAIGN or BRAND_LINK; match the campaign's own type |
| ttoAccountId | string | TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"page": {
"type": "number",
"description": "page number, from 1"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD (UTC+0), paired with startDate"
},
"pageSize": {
"type": "number",
"description": "1-100, default 25"
},
"startDate": {
"type": "string",
"description": "YYYY-MM-DD (UTC+0), paired with endDate: together they add per-day figures"
},
"campaignId": {
"type": "string",
"description": "TikTok One campaign id from list_tiktok_tto_campaigns"
},
"countryCode": {
"type": "string",
"description": "one of the campaign’s own country codes: resolved automatically when it has only one"
},
"campaignType": {
"enum": [
"CAMPAIGN",
"BRAND_LINK"
],
"type": "string",
"description": "CAMPAIGN or BRAND_LINK; match the campaign's own type"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
}
}
}| merchantCenterId* |
| string |
| from list_merchant_accounts |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"actionContext",
"actionFlowId"
],
"properties": {
"confirm": {
"type": "boolean",
"description": "must be true to actually fire it"
},
"inputValues": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "values for the flow's input fields, shaped as Google described them: {inputFieldId, textInputValue|choiceInputValue|checkboxInputValue}"
},
"actionFlowId": {
"type": "string",
"description": "the id of the flow you picked from that action's flows — each asks for different inputs"
},
"languageCode": {
"type": "string",
"description": "BCP-47 language for the response message"
},
"actionContext": {
"type": "string",
"description": "the opaque token from merchant_issue_help, on the action you are firing"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
}
}
}| Must still be TOMORROW or later. |
| clientName | string | client name shown on the invoice |
| adAccountId | string | Exactly one — Apple rejects more. |
| invoiceName | string | name shown on the invoice |
| orderNumber | string | purchase order or internal order number |
| billingEmail | string | billing contact email for invoices |
| primaryBuyerName | string | name of the primary buyer on the invoice |
| primaryBuyerEmail | string | email address Apple bills and notifies for this budget order |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"description": "REQUIRED."
},
"name": {
"type": "string",
"description": "new budget order name"
},
"amount": {
"type": "string",
"description": "New total, e.g. \"75000.00\". Raise it to extend a budget nearing exhaustion."
},
"endTime": {
"type": "string",
"description": "Can only SHORTEN an active budget order’s window."
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code"
},
"openEnded": {
"type": "boolean",
"description": "true clears the end date entirely, making the budget open-ended. This is the only way to extend rather than shorten."
},
"startTime": {
"type": "string",
"description": "Must still be TOMORROW or later."
},
"clientName": {
"type": "string",
"description": "client name shown on the invoice"
},
"adAccountId": {
"type": "string",
"description": "Exactly one — Apple rejects more."
},
"invoiceName": {
"type": "string",
"description": "name shown on the invoice"
},
"orderNumber": {
"type": "string",
"description": "purchase order or internal order number"
},
"billingEmail": {
"type": "string",
"description": "billing contact email for invoices"
},
"primaryBuyerName": {
"type": "string",
"description": "name of the primary buyer on the invoice"
},
"primaryBuyerEmail": {
"type": "string",
"description": "email address Apple bills and notifies for this budget order"
}
}
}| string |
| REQUIRED. |
| rules | array | Location group only (DYNAMIC). REPLACES every rule and puts the group back to PENDING. |
| endTime | string | end time, ISO 8601 |
| currency | string | ISO 4217 currency code |
| countries | array | Campaign only. REPLACES the whole list. |
| startTime | string | start time, ISO 8601 |
| 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. |
| defaultBid | string | Ad group only. |
| dailyBudget | string | Campaign only. |
| description | string | Location group only. |
| locationIds | array | Location group only (STATIC). REPLACES the whole membership. |
| searchMatch | boolean | Ad group only. |
| budgetOrderIds | array | 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. |
| bidStrategyType | string | Campaign only. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "REQUIRED."
},
"bid": {
"type": "string",
"description": "Keyword only."
},
"name": {
"type": "string",
"description": "new name for the campaign, ad group, creative, ad or location group"
},
"level": {
"enum": [
"campaign",
"adgroup",
"keyword",
"creative",
"ad",
"location_group"
],
"type": "string",
"description": "REQUIRED."
},
"rules": {
"type": "array",
"items": {
"type": "object",
"required": [
"field",
"operator",
"value"
],
"properties": {
"field": {
"type": "string",
"description": "rule field: adminArea (full state name), locality, postalCode or locationId"
},
"value": {
"description": "string for EQUALS or NOT_EQUALS; array for IN or NOT_IN"
},
"operator": {
"type": "string",
"description": "EQUALS, NOT_EQUALS, IN or NOT_IN"
}
}
},
"description": "Location group only (DYNAMIC). REPLACES every rule and puts the group back to PENDING."
},
"endTime": {
"type": "string",
"description": "end time, ISO 8601"
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "Campaign only. REPLACES the whole list."
},
"startTime": {
"type": "string",
"description": "start time, ISO 8601"
},
"targeting": {
"type": "object",
"description": "Ad group only. Same shape as create_apple_ads_ad_group.targeting — locationGroupIds, radius, adminAreaIds, daypart and the rest. REPLACES the dimensions you name.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"defaultBid": {
"type": "string",
"description": "Ad group only."
},
"dailyBudget": {
"type": "string",
"description": "Campaign only."
},
"description": {
"type": "string",
"description": "Location group only."
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Location group only (STATIC). REPLACES the whole membership."
},
"searchMatch": {
"type": "boolean",
"description": "Ad group only."
},
"budgetOrderIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"bidStrategyType": {
"enum": [
"MANUAL_CPT",
"MAX_CONVERSIONS"
],
"type": "string",
"description": "Campaign only."
}
}
}| campaignId* | string | AppLovin campaign id |
| websiteUrl | string | landing page URL for a WEB campaign |
| dailyBudget | number | new daily budget in account currency, all countries |
| countryGoals | object | per-country goal values keyed by ISO code |
| impressionUrl | string | impression-tracking URL for app campaigns |
| countryBudgets | object | per-country daily budgets keyed by ISO code, e.g. {US: 50} |
| audienceStrategy | string | audience strategy: UNIVERSAL, PROSPECTING or DISCOVERY |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"name": {
"type": "string",
"description": "new campaign name"
},
"confirm": {
"type": "boolean",
"description": "required for a spend change on a LIVE campaign"
},
"endDate": {
"type": "string",
"description": "YYYY-MM-DD"
},
"clickUrl": {
"type": "string",
"description": "click-tracking URL for app campaigns"
},
"usMetros": {
"type": "array",
"items": {
"type": "string"
},
"description": "US metro names such as NEW_YORK; not together with usRegions"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO country codes the campaign runs in"
},
"goalValue": {
"type": "number",
"description": "new target cost, or ROAS ratio (0.9 = 90%)"
},
"usRegions": {
"type": "array",
"items": {
"type": "string"
},
"description": "US state codes; only when US is a target country"
},
"campaignId": {
"type": "string",
"description": "AppLovin campaign id"
},
"websiteUrl": {
"type": "string",
"description": "landing page URL for a WEB campaign"
},
"dailyBudget": {
"type": "number",
"description": "new daily budget in account currency, all countries"
},
"countryGoals": {
"type": "object",
"description": "per-country goal values keyed by ISO code",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "number"
}
},
"impressionUrl": {
"type": "string",
"description": "impression-tracking URL for app campaigns"
},
"countryBudgets": {
"type": "object",
"description": "per-country daily budgets keyed by ISO code, e.g. {US: 50}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "number"
}
},
"audienceStrategy": {
"enum": [
"UNIVERSAL",
"PROSPECTING",
"DISCOVERY"
],
"type": "string",
"description": "audience strategy: UNIVERSAL, PROSPECTING or DISCOVERY"
}
}
}| string |
| AppLovin creative set id to edit |
| addToCampaignIds | array | also run it in these campaigns (up to 20) |
| useAccountBrandInfo | boolean | true = go back to the account-level brand info |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"creativeSetId"
],
"properties": {
"url": {
"type": "string",
"description": "landing page for this set; \"\" = the campaign’s"
},
"name": {
"type": "string",
"description": "new creative set name"
},
"confirm": {
"type": "boolean",
"description": "required to add a LIVE set to a LIVE campaign"
},
"assetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REPLACES the set’s assets"
},
"brandInfo": {
"type": "object",
"required": [
"title",
"description",
"buttonLabel",
"buttonColor",
"iconAssetId"
],
"properties": {
"title": {
"type": "string",
"description": "≤25 characters"
},
"buttonColor": {
"type": "string",
"description": "\"#RRGGBB\""
},
"buttonLabel": {
"type": "string",
"description": "CTA, ≤13 characters"
},
"description": {
"type": "string",
"description": "≤75 characters"
},
"iconAssetId": {
"type": "string",
"description": "an uploaded IMG_ICON asset id"
}
},
"description": "override the account brand on THIS creative set — how a brand A/B tests the end card"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "ISO codes; [] = all countries"
},
"languages": {
"type": "array",
"items": {
"type": "string"
},
"description": "e.g. ENGLISH; [] = all"
},
"creativeSetId": {
"type": "string",
"description": "AppLovin creative set id to edit"
},
"addToCampaignIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "also run it in these campaigns (up to 20)"
},
"useAccountBrandInfo": {
"type": "boolean",
"description": "true = go back to the account-level brand info"
}
}
}| website domain |
| summary | string | one-line description |
| audience | string | who the brand sells to, one line |
| category | string | product or business category, one line |
| logoDark | string | for light pictures |
| logoLight | string | for dark pictures; "none" removes |
| pronounce | string | how the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. "KOH-dee-ak"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name. |
| positioning | string | how the brand is positioned against alternatives, one line |
| profileType | string | website, social, describe or explore; explore keeps the profile but stops using it |
| pronunciations | object | how PRODUCT names are said aloud, e.g. {"Power Cakes": "POW-er cakes"}. Merged into the saved ones; an empty string removes one. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goal": {
"type": "string",
"description": "current marketing goal"
},
"logo": {
"type": "string",
"description": "logo image: URL or local file path to a PNG, JPEG or WebP (no SVG)"
},
"name": {
"type": "string",
"description": "brand name"
},
"brand": {
"type": "string",
"description": "profile id/name from list_brands, this call only"
},
"sells": {
"type": "string",
"description": "what the brand sells"
},
"style": {
"type": "string",
"description": "visual style — palette, typography, aesthetic"
},
"voice": {
"type": "string",
"description": "brand voice/tone"
},
"domain": {
"type": "string",
"description": "website domain"
},
"summary": {
"type": "string",
"description": "one-line description"
},
"audience": {
"type": "string",
"description": "who the brand sells to, one line"
},
"category": {
"type": "string",
"description": "product or business category, one line"
},
"logoDark": {
"type": "string",
"description": "for light pictures"
},
"logoLight": {
"type": "string",
"description": "for dark pictures; \"none\" removes"
},
"pronounce": {
"type": "string",
"description": "how the brand NAME is said aloud, as a simple respelling with the stressed syllable in capitals (e.g. \"KOH-dee-ak\"). Videos use it as a delivery note beside the spoken line; set it when a render mispronounced the name."
},
"positioning": {
"type": "string",
"description": "how the brand is positioned against alternatives, one line"
},
"profileType": {
"enum": [
"website",
"social",
"describe",
"explore"
],
"type": "string",
"description": "website, social, describe or explore; explore keeps the profile but stops using it"
},
"pronunciations": {
"type": "object",
"description": "how PRODUCT names are said aloud, e.g. {\"Power Cakes\": \"POW-er cakes\"}. Merged into the saved ones; an empty string removes one.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
}
}
}| documentId |
| string |
| the document id (from create_doc, or list_drive_files for one the user picked) |
| confirmCells | number | echo back the character count the unconfirmed call reported (rewrite only) |
| replacements | array | find/replace pairs, applied in order |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"docUrl": {
"type": "string",
"description": "a Google Docs URL — the id is extracted from it"
},
"confirm": {
"type": "boolean",
"description": "true runs a whole-body rewrite after the character count is shown"
},
"rewrite": {
"type": "string",
"description": "replace the WHOLE body with this text (\"\" empties the doc)"
},
"dropdowns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tabId": {
"type": "string",
"description": "tab id holding the dropdown, when the document has several tabs"
},
"title": {
"type": "string",
"description": "the dropdown title (from read_doc)"
},
"value": {
"type": "string",
"description": "the option to select, by its display text"
},
"options": {
"type": "array",
"items": {
"type": "string"
},
"description": "change the dropdown itself: the COMPLETE new option list (2–50) by display text; existing options keep their colour; shared by every chip built from it"
},
"replace": {
"type": "object",
"description": "for an option you remove that a chip shows: {\"Old\": \"New\"}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"newTitle": {
"type": "string",
"description": "rename the dropdown"
},
"dropdownId": {
"type": "string",
"description": "when two dropdowns share a title"
}
}
},
"description": "dropdown chips to set, or dropdowns to change"
},
"documentId": {
"type": "string",
"description": "the document id (from create_doc, or list_drive_files for one the user picked)"
},
"confirmCells": {
"type": "number",
"description": "echo back the character count the unconfirmed call reported (rewrite only)"
},
"replacements": {
"type": "array",
"items": {
"type": "object",
"required": [
"find"
],
"properties": {
"find": {
"type": "string",
"description": "exact text to find, as read_doc shows it; zero matches is reported as a failure"
},
"replace": {
"type": "string",
"description": "text to put in place of each match"
},
"matchCase": {
"type": "boolean",
"description": "false ignores letter case when matching; default matches case"
}
}
},
"description": "find/replace pairs, applied in order"
}
}
}| landingPage | string | new click destination, full https URL |
| callToAction | string | button label on the ad; needs a landing page on the ad |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"creativeId"
],
"properties": {
"name": {
"type": "string",
"description": "creative name shown in Campaign Manager"
},
"text": {
"type": "string",
"description": "new ad copy"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to edit an ACTIVE ad"
},
"creativeId": {
"type": "string",
"description": "from list_linkedin_ads_campaigns(campaignId) or create_linkedin_ads_creative"
},
"leadButton": {
"enum": [
"APPLY",
"DOWNLOAD",
"VIEW_QUOTE",
"LEARN_MORE",
"SIGN_UP",
"SUBSCRIBE",
"REGISTER",
"REQUEST_DEMO",
"JOIN",
"ATTEND",
"UNLOCK_FULL_DOCUMENT",
"SHOP_NOW",
"BUY_NOW",
"SEE_MORE"
],
"type": "string",
"description": "lead gen button label; only while the ad is DRAFT"
},
"leadFormId": {
"type": "string",
"description": "DRAFT creatives only — the lead gen form the ad opens"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
},
"landingPage": {
"type": "string",
"description": "new click destination, full https URL"
},
"callToAction": {
"enum": [
"APPLY",
"DOWNLOAD",
"VIEW_QUOTE",
"LEARN_MORE",
"SIGN_UP",
"SUBSCRIBE",
"REGISTER",
"JOIN",
"ATTEND",
"REQUEST_DEMO",
"SEE_MORE",
"BUY_NOW",
"SHOP_NOW"
],
"type": "string",
"description": "button label on the ad; needs a landing page on the ad"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"conversionId"
],
"properties": {
"name": {
"type": "string",
"description": "rule name shown in Campaign Manager and reports"
},
"enabled": {
"type": "boolean",
"description": "false RETIRES it — streamed events are then refused"
},
"adAccountId": {
"type": "string",
"description": "LinkedIn ad account id shared with this profile; needed only when several are shared"
},
"conversionId": {
"type": "string",
"description": "the rule id, e.g. \"104012\""
},
"attributionType": {
"enum": [
"LAST_TOUCH_BY_CAMPAIGN",
"LAST_TOUCH_BY_CONVERSION"
],
"type": "string",
"description": "how conversions are credited across campaigns: last touch by campaign or by conversion"
},
"postClickAttributionWindowSize": {
"type": "number",
"description": "post-click window in days: 1, 7, 30 or 90 (365 for the long-window types)"
},
"viewThroughAttributionWindowSize": {
"type": "number",
"description": "view-through window in days: 1, 7, 30 or 90 (365 for the long-window types)"
}
}
}| adAccountId | string | ad account that owns the form; use this or pageId |
| description | string | form description shown under the headline |
| thankYouCta | string | thank-you button label: visit website, learn more, view now, download or try now |
| legalDisclaimer | string | disclaimer text, 1 to 2000 characters |
| thankYouMessage | string | message shown to the person after they submit the form |
| privacyPolicyUrl | string | privacy policy URL shown on the form |
| thankYouLandingPage | string | URL the thank-you button opens |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"formId"
],
"properties": {
"name": {
"type": "string",
"description": "form name shown in Campaign Manager"
},
"state": {
"enum": [
"PUBLISHED",
"ARCHIVED"
],
"type": "string",
"description": "PUBLISHED keeps the form live; ARCHIVED retires it"
},
"formId": {
"type": "string",
"description": "form id"
},
"pageId": {
"type": "string",
"description": "Omit when one Page is shared"
},
"consents": {
"type": "array",
"items": {},
"description": "consent checkbox texts, or {text, required: true} for a mandatory one"
},
"headline": {
"type": "string",
"description": "title of the form people see"
},
"questions": {
"type": "array",
"items": {},
"description": "the fields to ask, in order: predefined names LinkedIn prefills from the member profile (FIRST_NAME, LAST_NAME, EMAIL, WORK_EMAIL, PHONE_NUMBER, COMPANY_NAME, JOB_TITLE, SENIORITY, COMPANY_SIZE, INDUSTRY, CITY, COUNTRY, ZIP_CODE, LINKEDIN_PROFILE_LINK …; friendly spellings like \"work email\" work) and/or custom questions {question, type:\"text\", maxLength?} or {question, options:[2–15 choices]}"
},
"adAccountId": {
"type": "string",
"description": "ad account that owns the form; use this or pageId"
},
"description": {
"type": "string",
"description": "form description shown under the headline"
},
"thankYouCta": {
"enum": [
"VISIT_COMPANY_WEBSITE",
"LEARN_MORE",
"VIEW_NOW",
"DOWNLOAD_NOW",
"TRY_NOW"
],
"type": "string",
"description": "thank-you button label: visit website, learn more, view now, download or try now"
},
"legalDisclaimer": {
"type": "string",
"description": "disclaimer text, 1 to 2000 characters"
},
"thankYouMessage": {
"type": "string",
"description": "message shown to the person after they submit the form"
},
"privacyPolicyUrl": {
"type": "string",
"description": "privacy policy URL shown on the form"
},
"thankYouLandingPage": {
"type": "string",
"description": "URL the thank-you button opens"
}
}
}| new product title; omit to leave unchanged |
| gender | string | male, female or unisex |
| offerId* | string | the product to change |
| ageGroup | string | newborn, infant, toddler, kids or adult |
| material | string | product material |
| condition | string | new | refurbished | used |
| feedLabel | string | market label, e.g. US; taken from the feed when it declares one |
| imageLink | string | main product image URL (https) |
| salePrice | object | new sale price, same shape as price |
| attributes | object | any other product attribute by its Merchant API name |
| dataSource | string | the feed it lives in, from list_merchant_data_sources |
| description | string | new product description; omit to leave unchanged |
| itemGroupId | string | shared id that groups the variants of one product |
| availability | string | in_stock | out_of_stock | preorder | backorder |
| productTypes | array | merchant-defined category labels, as a list |
| contentLanguage | string | two-letter language code; taken from the feed when it declares one |
| identifierExists | boolean | false for a product with no GTIN, MPN or brand, such as a service or bundle |
| merchantCenterId* | string | from list_merchant_accounts |
| additionalImageLinks | array | extra image URLs, sent as the whole list |
| googleProductCategory | string | Google product taxonomy category |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"offerId"
],
"properties": {
"mpn": {
"type": "string",
"description": "manufacturer part number"
},
"gtin": {
"type": "string",
"description": "GTIN barcode (UPC, EAN or similar)"
},
"link": {
"type": "string",
"description": "product page URL on the merchant's store"
},
"size": {
"type": "string",
"description": "product size"
},
"brand": {
"type": "string",
"description": "brand name"
},
"color": {
"type": "string",
"description": "product colour"
},
"price": {
"type": "object",
"required": [
"currencyCode"
],
"properties": {
"value": {
"type": "number",
"description": "major units, e.g. 19.99"
},
"amountMicros": {
"type": "string",
"description": "micros, if you already have them — 1 unit = 1000000"
},
"currencyCode": {
"type": "string",
"description": "three-letter ISO 4217, e.g. USD"
}
},
"description": "new price: {value in major units or amountMicros, currencyCode}"
},
"title": {
"type": "string",
"description": "new product title; omit to leave unchanged"
},
"gender": {
"type": "string",
"description": "male, female or unisex"
},
"offerId": {
"type": "string",
"description": "the product to change"
},
"ageGroup": {
"type": "string",
"description": "newborn, infant, toddler, kids or adult"
},
"material": {
"type": "string",
"description": "product material"
},
"condition": {
"type": "string",
"description": "new | refurbished | used"
},
"feedLabel": {
"type": "string",
"description": "market label, e.g. US; taken from the feed when it declares one"
},
"imageLink": {
"type": "string",
"description": "main product image URL (https)"
},
"salePrice": {
"type": "object",
"required": [
"currencyCode"
],
"properties": {
"value": {
"type": "number",
"description": "major units, e.g. 19.99"
},
"amountMicros": {
"type": "string",
"description": "micros, if you already have them — 1 unit = 1000000"
},
"currencyCode": {
"type": "string",
"description": "three-letter ISO 4217, e.g. USD"
}
},
"description": "new sale price, same shape as price"
},
"attributes": {
"type": "object",
"description": "any other product attribute by its Merchant API name",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"dataSource": {
"type": "string",
"description": "the feed it lives in, from list_merchant_data_sources"
},
"description": {
"type": "string",
"description": "new product description; omit to leave unchanged"
},
"itemGroupId": {
"type": "string",
"description": "shared id that groups the variants of one product"
},
"availability": {
"type": "string",
"description": "in_stock | out_of_stock | preorder | backorder"
},
"productTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "merchant-defined category labels, as a list"
},
"contentLanguage": {
"type": "string",
"description": "two-letter language code; taken from the feed when it declares one"
},
"identifierExists": {
"type": "boolean",
"description": "false for a product with no GTIN, MPN or brand, such as a service or bundle"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
},
"additionalImageLinks": {
"type": "array",
"items": {
"type": "string"
},
"description": "extra image URLs, sent as the whole list"
},
"googleProductCategory": {
"type": "string",
"description": "Google product taxonomy category"
}
}
}| adAccountId* | string | ad account id (for auth + scope) |
| dailyBudgetUsd | number | new daily budget in the AD ACCOUNT's currency (historical name — not necessarily USD; ad-set or campaign level); the reply reads it back with the currency code |
| dsaBeneficiary | string | ad sets: EU ad transparency, who the ad promotes. Meta requires it once targeting reaches the EU; retarget into the EU without it and the ad account default, else the profile's brand name, else the Page name is used |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"objectId",
"adAccountId"
],
"properties": {
"name": {
"type": "string",
"description": "new name"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED"
],
"type": "string",
"description": "ACTIVE starts spend (needs confirm:true); PAUSED / ARCHIVED are safe"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true ONLY to set status ACTIVE (real spend)"
},
"dsaPayor": {
"type": "string",
"description": "ad sets: EU ad transparency, who pays (defaults to dsaBeneficiary)"
},
"objectId": {
"type": "string",
"description": "the campaign / ad set / ad id (from list_meta_ads)"
},
"targeting": {
"type": "object",
"description": "replacement targeting spec (ad sets) — a Meta targeting object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "ad account id (for auth + scope)"
},
"dailyBudgetUsd": {
"type": "number",
"description": "new daily budget in the AD ACCOUNT's currency (historical name — not necessarily USD; ad-set or campaign level); the reply reads it back with the currency code"
},
"dsaBeneficiary": {
"type": "string",
"description": "ad sets: EU ad transparency, who the ad promotes. Meta requires it once targeting reaches the EU; retarget into the EU without it and the ad account default, else the profile's brand name, else the Page name is used"
}
}
}| public image URL (or the brand home page) to upload and assign as the account favicon — minimum 128×128 |
| accountName | string | internal; no review |
| faviconFileId | string | an already-uploaded favicon file id, if you have one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "the account’s primary destination — a public http(s) URL. OpenAI accepts this field even though their published parameter table omits it."
},
"name": {
"type": "string",
"description": "legacy: sets all three names"
},
"confirm": {
"type": "boolean",
"description": "required for a brand or legal change"
},
"brandName": {
"type": "string",
"description": "shown in ads; pauses delivery until review (confirm)"
},
"legalName": {
"type": "string",
"description": "restarts business review (confirm)"
},
"faviconUrl": {
"type": "string",
"description": "public image URL (or the brand home page) to upload and assign as the account favicon — minimum 128×128"
},
"accountName": {
"type": "string",
"description": "internal; no review"
},
"faviconFileId": {
"type": "string",
"description": "an already-uploaded favicon file id, if you have one"
}
}
}| number | string |
| REQUIRED for replace, optional for add/remove — membershipRevision from get_openai_ads_audience |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId",
"operation",
"members"
],
"properties": {
"members": {
"type": "array",
"items": {
"type": "string"
},
"description": "emails and/or phone numbers (already-SHA256-hashed emails pass through as-is)"
},
"operation": {
"enum": [
"add",
"remove",
"replace"
],
"type": "string",
"description": "add | remove | replace — replace swaps the entire membership"
},
"audienceId": {
"type": "string",
"description": "ChatGPT Ads audience id, from list_openai_ads_audiences"
},
"expectedRevision": {
"type": [
"number",
"string"
],
"description": "REQUIRED for replace, optional for add/remove — membershipRevision from get_openai_ads_audience"
}
}
}| confirm |
| boolean |
| REQUIRED true to change budget / bid / creative on a LIVE object |
| endTime | number | unix seconds when the campaign stops; campaign level |
| creative | object | REPLACES the ad’s creative (text + image card only) |
| adGroupId | string | ad group id to edit; use with level adGroup |
| countries | array | 2-letter country codes; replaces the whole geo list, so send the full list |
| platforms | array | 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. |
| targetCpa | number | Target CPA in the account currency: the average cost per conversion OpenAI aims for (bidStrategy target_cpa, conversions campaign, from 2026-11-09). Billing stays per click. Not with maxBid. |
| targetCpc | number | Target CPC in the account currency: the average cost per click OpenAI aims for (bidStrategy target_cpc, clicks campaign, from 2026-11-09). Not with maxBid. |
| campaignId | string | campaign id to edit; use with level campaign |
| bidStrategy | string | CHANGE HOW THIS AD GROUP BIDS. BIDDING IS REPLACED WHOLESALE, so a patch that moves the bid without restating the strategy would DEMOTE a maximize_* or target_* ad group, and one that sets a strategy without restating maxBid / the target DELETES it — both are refused by name with what would have been lost. To move an existing Max Bid / Max CPC ad group (fixed_bid, retired 2027-01-01) onto a target from 2026-11-09, pass bidStrategy target_cpa + targetCpa (conversions campaign) or target_cpc + targetCpc (clicks campaign); the old maxBid is dropped on purpose because OpenAI refuses both together. maximize_clicks / maximize_conversions is intelligent bidding. |
| dailyBudget | number | new daily budget in the account currency; campaign level |
| description | string | new description |
| locationIds | array | location ids from openai_ads_geo_search; replaces the whole list |
| billingEvent | string | required alongside a maxBid — bidding is replaced wholesale; maximize_* and target_* are click-billed and need no billingEvent |
| contextHints | array | REPLACES the existing list |
| lifetimeBudget | number | new lifetime budget in the account currency; campaign level |
| customAudienceIds | array | 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. |
| audienceBidMultipliers | array | 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. |
| 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. |
| conversionEventSettingIds | array | 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. |
| excludedCustomAudienceIds | array | REPLACES the campaign’s excluded custom audiences — same wholesale rule as customAudienceIds. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"adId": {
"type": "string",
"description": "ad id to edit; use with level ad"
},
"name": {
"type": "string",
"description": "new name"
},
"level": {
"enum": [
"campaign",
"adGroup",
"ad"
],
"type": "string",
"description": "inferred from which id you pass"
},
"maxBid": {
"type": "number",
"description": "DEPRECATED — Max Bid / Max CPC, retired by ChatGPT Ads on 2027-01-01: a fixed bid in the account currency (bidStrategy fixed_bid; on a conversions campaign it is the CPA bid). Needed only on an impressions campaign (per impression); otherwise use intelligent bidding or a target."
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change budget / bid / creative on a LIVE object"
},
"endTime": {
"type": "number",
"description": "unix seconds when the campaign stops; campaign level"
},
"creative": {
"type": "object",
"required": [
"title",
"body",
"targetUrl"
],
"properties": {
"body": {
"type": "string",
"description": "the description under the headline — 100 characters maximum, enforced"
},
"price": {
"type": "string",
"description": "optional price string shown on the card"
},
"title": {
"type": "string",
"description": "the headline — 3 to 50 characters, enforced"
},
"fileId": {
"type": "string",
"description": "reuse an image ChatGPT Ads already holds: the fileId list_openai_ads_campaigns returns for an existing ad (list with adGroupId). Wins over imageUrl; use it to copy an ad exactly"
},
"fileIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "the same as imageUrls with images already on the account (4–6 file ids); may be mixed with imageUrls, ids first. On an edit, omit both to keep the ad’s current images; a new list replaces them all"
},
"imageUrl": {
"type": "string",
"description": "public https URL of a STILL image — every text ad needs imageUrl OR fileId (ChatGPT Ads refuses an ad with no creative.file_id unless the campaign is a product feed; measured 2026-09-06); a video URL is refused, this channel has no video format"
},
"imageUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "4–6 public https URLs of EXACTLY square (1:1) stills, in display order, for an ad with MULTIPLE images (one title, body and landing page shared by all). Each URL is measured before anything is uploaded: OpenAI refuses an image even 1 pixel off square, and a set of 16:9 images. OpenAI enables this per ad account, and with automatic bidding only if the account is also enabled for that — otherwise use a fixed bid. Not with imageUrl/fileId or price"
},
"targetUrl": {
"type": "string",
"description": "the landing page (must not block OAI-AdsBot / OAI-SearchBot in robots.txt)"
}
},
"description": "REPLACES the ad’s creative (text + image card only)"
},
"adGroupId": {
"type": "string",
"description": "ad group id to edit; use with level adGroup"
},
"countries": {
"type": "array",
"items": {
"type": "string"
},
"description": "2-letter country codes; replaces the whole geo list, so send the full list"
},
"platforms": {
"type": "array",
"items": {
"enum": [
"ios_app",
"android_app",
"web",
"desktop_web",
"ios_web",
"android_web"
],
"type": "string"
},
"description": "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."
},
"targetCpa": {
"type": "number",
"description": "Target CPA in the account currency: the average cost per conversion OpenAI aims for (bidStrategy target_cpa, conversions campaign, from 2026-11-09). Billing stays per click. Not with maxBid."
},
"targetCpc": {
"type": "number",
"description": "Target CPC in the account currency: the average cost per click OpenAI aims for (bidStrategy target_cpc, clicks campaign, from 2026-11-09). Not with maxBid."
},
"campaignId": {
"type": "string",
"description": "campaign id to edit; use with level campaign"
},
"bidStrategy": {
"enum": [
"fixed_bid",
"maximize_clicks",
"maximize_conversions",
"target_cpc",
"target_cpa"
],
"type": "string",
"description": "CHANGE HOW THIS AD GROUP BIDS. BIDDING IS REPLACED WHOLESALE, so a patch that moves the bid without restating the strategy would DEMOTE a maximize_* or target_* ad group, and one that sets a strategy without restating maxBid / the target DELETES it — both are refused by name with what would have been lost. To move an existing Max Bid / Max CPC ad group (fixed_bid, retired 2027-01-01) onto a target from 2026-11-09, pass bidStrategy target_cpa + targetCpa (conversions campaign) or target_cpc + targetCpc (clicks campaign); the old maxBid is dropped on purpose because OpenAI refuses both together. maximize_clicks / maximize_conversions is intelligent bidding."
},
"dailyBudget": {
"type": "number",
"description": "new daily budget in the account currency; campaign level"
},
"description": {
"type": "string",
"description": "new description"
},
"locationIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "location ids from openai_ads_geo_search; replaces the whole list"
},
"billingEvent": {
"enum": [
"click",
"impression"
],
"type": "string",
"description": "required alongside a maxBid — bidding is replaced wholesale; maximize_* and target_* are click-billed and need no billingEvent"
},
"contextHints": {
"type": "array",
"items": {
"type": "string"
},
"description": "REPLACES the existing list"
},
"lifetimeBudget": {
"type": "number",
"description": "new lifetime budget in the account currency; campaign level"
},
"customAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"audienceBidMultipliers": {
"type": "array",
"items": {
"type": "object",
"required": [
"customAudienceId",
"multiplier"
],
"properties": {
"multiplier": {
"type": "number",
"description": "0.1 to 10; 2 bids twice as much for this audience"
},
"customAudienceId": {
"type": "string",
"description": "custom audience id to bid more or less for, from list_openai_ads_audiences"
}
}
},
"description": "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."
},
"landingPageQueryTemplate": {
"type": "string",
"description": "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."
},
"conversionEventSettingIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "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."
},
"excludedCustomAudienceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REPLACES the campaign’s excluded custom audiences — same wholesale rule as customAudienceIds."
}
}
}| adGroupId | string | move the ad into a different ad group |
| leadFormId | string | Pinterest lead form id for a lead ad |
| adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
| iosDeepLink | string | iOS deep link for an app ad |
| creativeType | string | creative format, e.g. REGULAR, VIDEO, CAROUSEL or APP |
| trackingUrls | object | Pinterest tracking URL object for the ad |
| destinationUrl | string | where the click goes |
| androidDeepLink | string | Android deep link for an app ad |
| viewTrackingUrl | string | view-through tracking URL |
| clickTrackingUrl | string | click-through tracking URL |
| carouselDestinationUrls | array | one destination URL per carousel card, in card order |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adId"
],
"properties": {
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term tag merged into the destination URL; Pinterest has no UTM field"
},
"medium": {
"type": "string",
"description": "utm_medium tag merged into the destination URL; Pinterest has no UTM field"
},
"source": {
"type": "string",
"description": "utm_source tag merged into the destination URL; Pinterest has no UTM field"
},
"content": {
"type": "string",
"description": "utm_content tag merged into the destination URL; Pinterest has no UTM field"
},
"campaign": {
"type": "string",
"description": "utm_campaign tag merged into the destination URL; Pinterest has no UTM field"
}
},
"description": "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"
},
"adId": {
"type": "string",
"description": "the ad to edit"
},
"name": {
"type": "string",
"description": "new ad name"
},
"pinId": {
"type": "string",
"description": "DRAFT ads only — Pinterest refuses it on any other status"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED",
"DRAFT"
],
"type": "string",
"description": "REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete"
},
"adGroupId": {
"type": "string",
"description": "move the ad into a different ad group"
},
"leadFormId": {
"type": "string",
"description": "Pinterest lead form id for a lead ad"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id, from list_pinterest_ads_campaigns"
},
"iosDeepLink": {
"type": "string",
"description": "iOS deep link for an app ad"
},
"creativeType": {
"enum": [
"REGULAR",
"VIDEO",
"SHOPPING",
"CAROUSEL",
"MAX_VIDEO",
"COLLECTION",
"IDEA",
"SHOWCASE",
"QUIZ",
"COLLAGE",
"APP"
],
"type": "string",
"description": "creative format, e.g. REGULAR, VIDEO, CAROUSEL or APP"
},
"trackingUrls": {
"type": "object",
"description": "Pinterest tracking URL object for the ad",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"destinationUrl": {
"type": "string",
"description": "where the click goes"
},
"androidDeepLink": {
"type": "string",
"description": "Android deep link for an app ad"
},
"viewTrackingUrl": {
"type": "string",
"description": "view-through tracking URL"
},
"clickTrackingUrl": {
"type": "string",
"description": "click-through tracking URL"
},
"carouselDestinationUrls": {
"type": "array",
"items": {
"type": "string"
},
"description": "one destination URL per carousel card, in card order"
}
}
}| status |
| string |
| REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete |
| confirm | boolean | REQUIRED true to change bid / budget / campaign on a LIVE ad group |
| endTime | number | Unix timestamp in SECONDS |
| adGroupId* | string | the ad group to edit |
| startTime | number | Unix timestamp in SECONDS |
| budgetType | string | budget mode: DAILY, LIFETIME or CBO_ADGROUP |
| campaignId | string | MOVE the ad group into a different campaign — it then spends from that budget |
| adAccountId | string | Pinterest ad account id, from list_pinterest_ads_campaigns |
| trackingUrls | object | Pinterest tracking URL object for the ad group |
| autoTargeting | boolean | true or false: Pinterest automatic targeting |
| billableEvent | string | Pinterest ties this to the campaign objective and refuses a mismatch |
| targetingSpec | object | REPLACES the whole targeting spec — get ids from search_pinterest_ads_targeting, never guess one |
| placementGroup | string | where the ads run: ALL, SEARCH, BROWSE or OTHER |
| lifetimeFrequencyCap | number | CPM (IMPRESSION-billed) ad groups only, and Pinterest requires endTime with it |
| targetingTemplateIds | array | Pinterest refuses these alongside targetingSpec / trackingUrls / autoTargeting / placementGroup |
| optimizationGoalMetadata | object | Pinterest optimization goal metadata object |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId"
],
"properties": {
"bid": {
"type": "number",
"description": "what you pay per billable event, in the ad account’s currency"
},
"name": {
"type": "string",
"description": "new ad group name"
},
"budget": {
"type": "number",
"description": "only on a campaign that is NOT budget-optimized"
},
"pacing": {
"enum": [
"STANDARD",
"ACCELERATED"
],
"type": "string",
"description": "delivery speed: STANDARD or ACCELERATED"
},
"status": {
"enum": [
"ACTIVE",
"PAUSED",
"ARCHIVED",
"DRAFT"
],
"type": "string",
"description": "REFUSED HERE ON PURPOSE — use set_pinterest_ads_status, which confirm-gates real spend and Pinterest’s archive-as-delete"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true to change bid / budget / campaign on a LIVE ad group"
},
"endTime": {
"type": "number",
"description": "Unix timestamp in SECONDS"
},
"adGroupId": {
"type": "string",
"description": "the ad group to edit"
},
"startTime": {
"type": "number",
"description": "Unix timestamp in SECONDS"
},
"budgetType": {
"enum": [
"DAILY",
"LIFETIME",
"CBO_ADGROUP"
],
"type": "string",
"description": "budget mode: DAILY, LIFETIME or CBO_ADGROUP"
},
"campaignId": {
"type": "string",
"description": "MOVE the ad group into a different campaign — it then spends from that budget"
},
"adAccountId": {
"type": "string",
"description": "Pinterest ad account id, from list_pinterest_ads_campaigns"
},
"trackingUrls": {
"type": "object",
"description": "Pinterest tracking URL object for the ad group",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"autoTargeting": {
"type": "boolean",
"description": "true or false: Pinterest automatic targeting"
},
"billableEvent": {
"enum": [
"CLICKTHROUGH",
"IMPRESSION",
"VIDEO_V_50_MRC"
],
"type": "string",
"description": "Pinterest ties this to the campaign objective and refuses a mismatch"
},
"targetingSpec": {
"type": "object",
"description": "REPLACES the whole targeting spec — get ids from search_pinterest_ads_targeting, never guess one",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"placementGroup": {
"enum": [
"ALL",
"SEARCH",
"BROWSE",
"OTHER"
],
"type": "string",
"description": "where the ads run: ALL, SEARCH, BROWSE or OTHER"
},
"lifetimeFrequencyCap": {
"type": "number",
"description": "CPM (IMPRESSION-billed) ad groups only, and Pinterest requires endTime with it"
},
"targetingTemplateIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Pinterest refuses these alongside targetingSpec / trackingUrls / autoTargeting / placementGroup"
},
"optimizationGoalMetadata": {
"type": "object",
"description": "Pinterest optimization goal metadata object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}| Reddit ad account id (a2_...); omit when only one is shared |
| previewDays | number | 1-30: (re)enable the preview link for this many days |
| eventTrackers | array | replaces the third-party trackers (Reddit-approved providers only) |
| redditProfileId | string | Reddit profile id (t2_...) the ad runs as |
| shoppingCreative | object | catalog ads: same fields as create_reddit_ads_ad |
| clickUrlQueryParameters | any | UTM / tracking parameters, e.g. {"utm_source":"reddit","utm_content":"{{AD_ID}}"} (≤14); null clears them |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adId"
],
"properties": {
"adId": {
"type": "string",
"description": "id of the ad to edit"
},
"name": {
"type": "string",
"description": "new ad name"
},
"postId": {
"type": "string",
"description": "promote a different post"
},
"clickUrl": {
"type": "string",
"description": "pass an empty string to clear it"
},
"previewOff": {
"type": "boolean",
"description": "true switches the preview link off"
},
"productIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "catalog ads: up to 6 featured product ids"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"previewDays": {
"type": "number",
"description": "1-30: (re)enable the preview link for this many days"
},
"eventTrackers": {
"type": "array",
"items": {
"type": "object",
"required": [
"type",
"url"
],
"properties": {
"url": {
"type": "string",
"description": "third-party measurement URL from a Reddit-approved provider"
},
"type": {
"enum": [
"CLICK",
"VIEW"
],
"type": "string",
"description": "CLICK or VIEW: when the third-party tracker fires"
}
}
},
"description": "replaces the third-party trackers (Reddit-approved providers only)"
},
"redditProfileId": {
"type": "string",
"description": "Reddit profile id (t2_...) the ad runs as"
},
"shoppingCreative": {
"type": "object",
"description": "catalog ads: same fields as create_reddit_ads_ad",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"clickUrlQueryParameters": {
"anyOf": [
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "UTM / tracking parameters, e.g. {\"utm_source\":\"reddit\",\"utm_content\":\"{{AD_ID}}\"} (≤14); null clears them"
}
}
}| id of the ad group to edit |
| bidAmount | number | new bid in the ad account currency |
| startTime | string | ISO 8601 new start time |
| 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 |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
| bidStrategy | string | BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX |
| 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 | string | 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 |
| savedAudienceId | string | point this ad group at a saved audience instead |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adGroupId"
],
"properties": {
"name": {
"type": "string",
"description": "new ad group name"
},
"budget": {
"type": "number",
"description": "new budget in the ad account currency, paired with goalType"
},
"bidType": {
"enum": [
"CPC",
"CPM",
"CPV",
"CPV6",
"CPV15"
],
"type": "string",
"description": "CPC, CPM, CPV, CPV6 or CPV15; must fit the campaign objective"
},
"endTime": {
"type": "string",
"description": "ISO 8601 new end time"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "DAILY_SPEND or LIFETIME_SPEND"
},
"schedule": {
"type": "array",
"items": {},
"description": "weekly delivery windows: startDay, endDay, startHour, endHour (0 = Sunday)"
},
"adGroupId": {
"type": "string",
"description": "id of the ad group to edit"
},
"bidAmount": {
"type": "number",
"description": "new bid in the ad account currency"
},
"startTime": {
"type": "string",
"description": "ISO 8601 new start time"
},
"targeting": {
"type": "object",
"description": "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",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"bidStrategy": {
"enum": [
"BIDLESS",
"MANUAL_BIDDING",
"MAXIMIZE_VOLUME",
"TARGET_CPX"
],
"type": "string",
"description": "BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX"
},
"productSetId": {
"type": "string",
"description": "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": {
"enum": [
"DYNAMIC",
"STATIC"
],
"type": "string",
"description": "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"
},
"savedAudienceId": {
"type": "string",
"description": "point this ad group at a saved audience instead"
}
}
}| Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"name": {
"type": "string",
"description": "new campaign name"
},
"budget": {
"type": "number",
"description": "new campaign budget in the ad account currency"
},
"endTime": {
"type": "string",
"description": "ISO 8601 new flight end"
},
"goalType": {
"enum": [
"DAILY_SPEND",
"LIFETIME_SPEND"
],
"type": "string",
"description": "DAILY_SPEND or LIFETIME_SPEND"
},
"spendCap": {
"type": "number",
"description": "lifetime spend ceiling in the ad account’s currency"
},
"startTime": {
"type": "string",
"description": "ISO 8601 new flight start"
},
"campaignId": {
"type": "string",
"description": "id of the campaign to edit"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}| schedule | object | feed only — same shape as create_reddit_ads_product_feed |
| username | string | feed only |
| adAccountId | string | Reddit ad account id (a2_...); omit when only one is shared |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"kind",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the catalog, feed or product set id"
},
"url": {
"type": "string",
"description": "feed only"
},
"kind": {
"enum": [
"catalog",
"feed",
"product_set"
],
"type": "string",
"description": "catalog, feed or product_set: which type the id refers to"
},
"mode": {
"enum": [
"REPLACE",
"UPDATE"
],
"type": "string",
"description": "feed only"
},
"name": {
"type": "string",
"description": "new name"
},
"filter": {
"description": "product_set only — the new rule"
},
"password": {
"type": "string",
"description": "feed only"
},
"pixelIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "catalog only — replaces the attached pixels"
},
"schedule": {
"type": "object",
"description": "feed only — same shape as create_reddit_ads_product_feed",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"username": {
"type": "string",
"description": "feed only"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
}
}
}| userGeneratedContent | string | OPT_IN or OPT_OUT of Reddit user-generated content enhancements |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adId"
],
"properties": {
"adId": {
"type": "string",
"description": "template ad id, from create_reddit_ads_max_campaign"
},
"name": {
"type": "string",
"description": "new template ad name"
},
"assetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "REPLACES the template’s assets"
},
"displayUrl": {
"type": "string",
"description": "new display URL; same domain as destinationUrl"
},
"adAccountId": {
"type": "string",
"description": "Reddit ad account id (a2_...); omit when only one is shared"
},
"destinationUrl": {
"type": "string",
"description": "new https landing page"
},
"supplementaryText": {
"type": "string",
"description": "up to 100 characters"
},
"thumbnailAssetIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "up to 10; an empty list clears them"
},
"userGeneratedContent": {
"enum": [
"OPT_IN",
"OPT_OUT"
],
"type": "string",
"description": "OPT_IN or OPT_OUT of Reddit user-generated content enhancements"
}
}
}| Google spreadsheet id; use this or sheetUrl |
| valueInputOption | string | USER_ENTERED (default) parses formulas, dates and numbers the way typing them would; RAW stores every value as literal text |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"range": {
"type": "string",
"description": "A1 range or anchor cell, e.g. \"B2:C5\", \"B2\", or \"Q3 Report!B2\" (default A1)"
},
"values": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "array of row arrays to write"
},
"confirm": {
"type": "boolean",
"description": "required only when the target range already holds values"
},
"updates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"range": {
"type": "string",
"description": "A1 range or anchor cell for this block, e.g. B2:C5 or Q3!B2"
},
"values": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": [
"string",
"number",
"boolean"
]
}
},
"description": "rows for this block, as an array of row arrays"
}
}
},
"description": "write SEVERAL disjoint ranges in one call, instead of range+values"
},
"sheetUrl": {
"type": "string",
"description": "Google Sheets link, instead of spreadsheetId"
},
"spreadsheetId": {
"type": "string",
"description": "Google spreadsheet id; use this or sheetUrl"
},
"valueInputOption": {
"enum": [
"USER_ENTERED",
"RAW"
],
"type": "string",
"description": "USER_ENTERED (default) parses formulas, dates and numbers the way typing them would; RAW stores every value as literal text"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"action": {
"enum": [
"add",
"remove",
"remove_all"
],
"type": "string",
"description": "default add"
},
"schema": {
"enum": [
"EMAIL",
"PHONE",
"MOBILE_AD_ID"
],
"type": "string",
"description": "default EMAIL"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true for remove and remove_all"
},
"audienceId": {
"type": "string",
"description": "FIRST_PARTY customer-list audience id from list_snapchat_ads_audiences"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"identifiers": {
"type": "array",
"items": {
"type": "string"
},
"description": "plain values or 64-hex SHA-256 hashes; up to 100,000 per request are batched"
}
}
}| boolean |
| set utm_source=snapchat, utm_medium=paid_social, utm_campaign={{campaign.name}}, utm_content={{ad.id}} |
| confirm | boolean | REQUIRED true — without it nothing changes and you get the before -> after |
| endTime | string | ISO 8601, after startTime |
| pixelId | string | Snap Pixel id the ad squad optimises toward; ad squads only |
| bidMicro | number | ONLY if already multiplied by 1,000,000; never with bid |
| headline | string | new headline, max 34 characters; creatives only |
| audiences | array | audience ids the squad TARGETS — replaces its audience targeting; [] clears it |
| brandName | string | new brand name, max 32 characters; creatives only |
| startTime | string | ISO 8601; on an ad squad it must be now or later |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| bidStrategy | string | AUTO_BID, LOWEST_COST_WITH_MAX_BID or TARGET_COST; ad squads only |
| callToAction | string | new call-to-action label; creatives only |
| landingPageUrl | string | https only, max 2048 characters |
| excludeAudiences | array | audience ids to exclude from the ad squad |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"level",
"id"
],
"properties": {
"id": {
"type": "string",
"description": "the object id (a creative id comes from the ad row’s creative_id or upload_snapchat_ads_creative)"
},
"bid": {
"type": "number",
"description": "ad squad bid in the ad account's currency (converted to micro here)"
},
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term on the landing URL; WEB_VIEW creatives only"
},
"medium": {
"type": "string",
"description": "utm_medium on the landing URL; WEB_VIEW creatives only"
},
"source": {
"type": "string",
"description": "utm_source on the landing URL; WEB_VIEW creatives only"
},
"content": {
"type": "string",
"description": "utm_content on the landing URL; WEB_VIEW creatives only"
},
"campaign": {
"type": "string",
"description": "utm_campaign on the landing URL; WEB_VIEW creatives only; Snapchat macros stay literal"
}
},
"description": "UTM parameters set on the landing page URL; Snapchat macros like {{campaign.name}} stay literal"
},
"name": {
"type": "string",
"description": "new name for the object"
},
"level": {
"enum": [
"campaign",
"adsquad",
"ad",
"creative"
],
"type": "string",
"description": "object to edit: campaign, adsquad, ad or creative"
},
"addUtm": {
"type": "boolean",
"description": "set utm_source=snapchat, utm_medium=paid_social, utm_campaign={{campaign.name}}, utm_content={{ad.id}}"
},
"confirm": {
"type": "boolean",
"description": "REQUIRED true — without it nothing changes and you get the before -> after"
},
"endTime": {
"type": "string",
"description": "ISO 8601, after startTime"
},
"pixelId": {
"type": "string",
"description": "Snap Pixel id the ad squad optimises toward; ad squads only"
},
"bidMicro": {
"type": "number",
"description": "ONLY if already multiplied by 1,000,000; never with bid"
},
"headline": {
"type": "string",
"description": "new headline, max 34 characters; creatives only"
},
"audiences": {
"type": "array",
"items": {
"type": "string"
},
"description": "audience ids the squad TARGETS — replaces its audience targeting; [] clears it"
},
"brandName": {
"type": "string",
"description": "new brand name, max 32 characters; creatives only"
},
"startTime": {
"type": "string",
"description": "ISO 8601; on an ad squad it must be now or later"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"bidStrategy": {
"enum": [
"AUTO_BID",
"LOWEST_COST_WITH_MAX_BID",
"TARGET_COST"
],
"type": "string",
"description": "AUTO_BID, LOWEST_COST_WITH_MAX_BID or TARGET_COST; ad squads only"
},
"callToAction": {
"type": "string",
"description": "new call-to-action label; creatives only"
},
"landingPageUrl": {
"type": "string",
"description": "https only, max 2048 characters"
},
"excludeAudiences": {
"type": "array",
"items": {
"type": "string"
},
"description": "audience ids to exclude from the ad squad"
}
}
}| boolean |
| true shows the promotion-code box at checkout; false hides it |
| afterCompletionMessage | string | message shown on Stripe's confirmation page after paying |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"paymentLinkId"
],
"properties": {
"active": {
"type": "boolean",
"description": "false deactivates the link; true reactivates it"
},
"confirm": {
"type": "boolean",
"description": "true to deactivate the link, and to confirm a live-key write"
},
"metadata": {
"type": "object",
"description": "key/value metadata to set on the link",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"paymentLinkId": {
"type": "string",
"description": "payment link id, plink_..., from list_stripe_payment_links"
},
"inactiveMessage": {
"type": "string",
"description": "message shown while the link is off, up to 500 characters"
},
"afterCompletionUrl": {
"type": "string",
"description": "URL the buyer is redirected to after paying"
},
"allowPromotionCodes": {
"type": "boolean",
"description": "true shows the promotion-code box at checkout; false hides it"
},
"afterCompletionMessage": {
"type": "string",
"description": "message shown on Stripe's confirmation page after paying"
}
}
}| gtmWorkspace | string | Tag Manager workspace id or name; defaults to the Default Workspace |
| containerPath* | string | container path accounts/<id>/containers/<id>, from list_tag_manager_containers |
| firingTriggerId | array | replaces the firing trigger ids, from list_tag_manager_tags |
| tagFiringOption | string | unlimited, oncePerEvent or oncePerLoad |
| blockingTriggerId | array | replaces the blocking trigger ids |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"containerPath",
"tagId"
],
"properties": {
"name": {
"type": "string",
"description": "new tag name"
},
"notes": {
"type": "string",
"description": "replaces the internal note on the tag"
},
"tagId": {
"type": "string",
"description": "the tagId from list_tag_manager_tags"
},
"paused": {
"type": "boolean",
"description": "pause the tag rather than removing its trigger, which is how you stop one running"
},
"parameter": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "replaces the tag parameters: [{type, key, value}]"
},
"workspace": {
"type": "string",
"description": "older name for gtmWorkspace (MCP only)"
},
"gtmWorkspace": {
"type": "string",
"description": "Tag Manager workspace id or name; defaults to the Default Workspace"
},
"containerPath": {
"type": "string",
"description": "container path accounts/<id>/containers/<id>, from list_tag_manager_containers"
},
"firingTriggerId": {
"type": "array",
"items": {
"type": "string"
},
"description": "replaces the firing trigger ids, from list_tag_manager_tags"
},
"tagFiringOption": {
"type": "string",
"description": "unlimited, oncePerEvent or oncePerLoad"
},
"blockingTriggerId": {
"type": "array",
"items": {
"type": "string"
},
"description": "replaces the blocking trigger ids"
}
}
}| videoId | string | swap the video (from upload_tiktok_ads_creative); pass its coverImageId as imageIds too |
| imageIds | array | image ids for the ad: the video's cover (coverImageId from upload_tiktok_ads_creative), or the slides for an image ad |
| 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. |
| advertiserId | string | TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| callToAction | string | TikTok’s CTA value, e.g. LEARN_MORE, SHOP_NOW, SIGN_UP, DOWNLOAD |
| tiktokItemId | string | swap the Spark post |
| landingPageUrl | string | full http(s) URL |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"adId"
],
"properties": {
"adId": {
"type": "string",
"description": "the ad to edit, from list_tiktok_ads_campaigns"
},
"name": {
"type": "string",
"description": "the ad name in TikTok Ads Manager"
},
"adText": {
"type": "string",
"description": "the copy people read, 1-100 characters, no emoji"
},
"pageId": {
"type": "string",
"description": "Instant Form id for a lead ad (checked first)"
},
"confirm": {
"type": "boolean",
"description": "required when the ad is switched on"
},
"musicId": {
"type": "string",
"description": "carousel sound, from list_tiktok_ads_music"
},
"videoId": {
"type": "string",
"description": "swap the video (from upload_tiktok_ads_creative); pass its coverImageId as imageIds too"
},
"imageIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "image ids for the ad: the video's cover (coverImageId from upload_tiktok_ads_creative), or the slides for an image ad"
},
"utmParams": {
"type": "object",
"description": "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.",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"callToAction": {
"type": "string",
"description": "TikTok’s CTA value, e.g. LEARN_MORE, SHOP_NOW, SIGN_UP, DOWNLOAD"
},
"tiktokItemId": {
"type": "string",
"description": "swap the Spark post"
},
"landingPageUrl": {
"type": "string",
"description": "full http(s) URL"
}
}
}| advertiserId |
| string |
| TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile |
| applyObjects* | array | same shape as create_tiktok_ads_rule |
| confirmScope | string | required in the same case — the scope token the refusal prints, computed from the version being sent |
| notification* | object | notification settings object (notificationType, emailSetting); replaces the existing one |
| ruleExecInfo* | object | run schedule object (execTimeType, execTime, timePeriodInfo); replaces the existing one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"ruleId",
"name",
"applyObjects",
"conditions",
"actions",
"notification",
"ruleExecInfo"
],
"properties": {
"name": {
"type": "string",
"description": "rule name; the update replaces the whole rule, so send the name again"
},
"tzone": {
"type": "string",
"description": "timezone for the rule's time windows, sent to TikTok as given"
},
"ruleId": {
"type": "string",
"description": "from list_tiktok_ads_rules"
},
"actions": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "what the rule does on a match; replaces the existing actions"
},
"confirm": {
"type": "boolean",
"description": "required when the rule is live and the new version can spend"
},
"conditions": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "when the rule fires; replaces the existing conditions"
},
"advertiserId": {
"type": "string",
"description": "TikTok ad account id from list_tiktok_ads_accounts; omit when only one account is shared with this profile"
},
"applyObjects": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "same shape as create_tiktok_ads_rule"
},
"confirmScope": {
"type": "string",
"description": "required in the same case — the scope token the refusal prints, computed from the version being sent"
},
"notification": {
"type": "object",
"description": "notification settings object (notificationType, emailSetting); replaces the existing one",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"ruleExecInfo": {
"type": "object",
"description": "run schedule object (execTimeType, execTime, timePeriodInfo); replaces the existing one",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"campaignId": {
"type": "string",
"description": "TikTok One campaign id from list_tiktok_tto_campaigns"
},
"handleNames": {
"type": "array",
"items": {
"type": "string"
},
"description": "creators to ADD: max 100, CAMPAIGN only"
},
"campaignType": {
"enum": [
"CAMPAIGN",
"BRAND_LINK"
],
"type": "string",
"description": "default CAMPAIGN"
},
"ttoAccountId": {
"type": "string",
"description": "TikTok One account id from list_tiktok_tto_accounts; omit when only one account is reachable"
},
"advertiserIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "ad accounts to ADD: max 50"
},
"sendNotification": {
"type": "boolean",
"description": "default false"
}
}
}| add or remove: whether the people in this call are added to or removed from the audience |
| audienceId* | string | custom audience id to change, from list_x_ads_audiences |
| twitterIds | array | X user ids to add; hashed on Hermoso's server before X receives them |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| description | string | new audience description |
| effectiveAt | string | ISO 8601 time the membership change takes effect (sent to X as effective_at) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"audienceId"
],
"properties": {
"name": {
"type": "string",
"description": "new audience name"
},
"users": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "one person per entry with several identifiers: {email, phone, handle, twitterId, deviceId}"
},
"emails": {
"type": "array",
"items": {
"type": "string"
},
"description": "plain addresses or 64-hex SHA-256 hashes"
},
"phones": {
"type": "array",
"items": {
"type": "string"
},
"description": "E.164, e.g. +14155550100, or hashes"
},
"handles": {
"type": "array",
"items": {
"type": "string"
},
"description": "X handles, with or without @"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"deviceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "IDFA / AdID / Android id"
},
"expiresAt": {
"type": "string",
"description": "ISO 8601 time the members expire; must be later than effectiveAt"
},
"operation": {
"enum": [
"add",
"remove"
],
"type": "string",
"description": "add or remove: whether the people in this call are added to or removed from the audience"
},
"audienceId": {
"type": "string",
"description": "custom audience id to change, from list_x_ads_audiences"
},
"twitterIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "X user ids to add; hashed on Hermoso's server before X receives them"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"description": {
"type": "string",
"description": "new audience description"
},
"effectiveAt": {
"type": "string",
"description": "ISO 8601 time the membership change takes effect (sent to X as effective_at)"
}
}
}| boolean |
| false = accelerated. Works on a campaign-budget campaign; with line item budgets set it on the line item |
| purchaseOrderNumber | string | purchase order number recorded on the campaign |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"campaignId"
],
"properties": {
"name": {
"type": "string",
"description": "new campaign name"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"campaignId": {
"type": "string",
"description": "from list_x_ads_campaigns"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"dailyBudget": {
"type": "number",
"description": "in the ad account's currency"
},
"totalBudget": {
"type": "number",
"description": "new lifetime budget in ad account currency"
},
"standardDelivery": {
"type": "boolean",
"description": "false = accelerated. Works on a campaign-budget campaign; with line item budgets set it on the line item"
},
"purchaseOrderNumber": {
"type": "string",
"description": "purchase order number recorded on the campaign"
}
}
}| string |
| intro card text |
| thankYouUrl | string | https link on the thank-you button |
| thankYouButton | string | thank-you page button label |
| privacyPolicyUrl | string | https link to the privacy policy shown on the form |
| thankYouHeadline | string | thank-you page headline; X replaces the whole section |
| conversionEventId | string | X Events Manager event id; needed when the account has no X Pixel |
| thankYouDescription | string | thank-you page text; X replaces the whole section |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"leadFormId"
],
"properties": {
"name": {
"type": "string",
"description": "new internal name of the lead form"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"enum": [
"EMAIL",
"PHONE_NUMBER",
"STREET_ADDRESS",
"CITY",
"STATE",
"COUNTRY",
"ZIP_CODE",
"FULL_NAME",
"FIRST_NAME",
"LAST_NAME",
"DATE_OF_BIRTH",
"COMPANY_NAME",
"JOB_TITLE",
"WORK_EMAIL",
"WORK_PHONE_NUMBER",
"GENDER",
"HANDLE"
],
"type": "string",
"description": "lead field the form collects, from X's fixed list of lead field keys"
},
"optional": {
"type": "boolean",
"description": "true = the person may skip this field; at least one field must not be optional"
}
}
},
"description": "replaces the whole field list; each entry as in create_x_ads_lead_form"
},
"purpose": {
"type": "string",
"description": "what the leads are used for, shown to the person"
},
"headline": {
"type": "string",
"description": "intro card headline"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"leadFormId": {
"type": "string",
"description": "lead form id from list_x_ads_lead_forms"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"companyName": {
"type": "string",
"description": "company name shown on the form"
},
"description": {
"type": "string",
"description": "intro card text"
},
"thankYouUrl": {
"type": "string",
"description": "https link on the thank-you button"
},
"thankYouButton": {
"type": "string",
"description": "thank-you page button label"
},
"privacyPolicyUrl": {
"type": "string",
"description": "https link to the privacy policy shown on the form"
},
"thankYouHeadline": {
"type": "string",
"description": "thank-you page headline; X replaces the whole section"
},
"conversionEventId": {
"type": "string",
"description": "X Events Manager event id; needed when the account has no X Pixel"
},
"thankYouDescription": {
"type": "string",
"description": "thank-you page text; X replaces the whole section"
}
}
}| ISO 8601 |
| lineItemId* | string | from list_x_ads_line_items |
| adAccountId | string | X ad account id (list_x_ads_accounts); omit if only one is shared |
| bidStrategy | string | AUTO, GUARANTEED, MAX or TARGET; MAX and TARGET need bidAmount |
| dailyBudget | number | only if the campaign is not budget-optimised |
| totalBudget | number | only if the campaign is not budget-optimised |
| standardDelivery | boolean | false = accelerated, only with line item budgets; X refuses it under a campaign budget |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"lineItemId"
],
"properties": {
"goal": {
"enum": [
"APP_CLICKS",
"APP_INSTALLS",
"APP_PURCHASES",
"ENGAGEMENT",
"FOLLOWERS",
"LINK_CLICKS",
"MAX_REACH",
"PREROLL",
"PREROLL_STARTS",
"REACH_WITH_ENGAGEMENT",
"SITE_VISITS",
"SOCIAL_ENGAGEMENT",
"VIDEO_VIEW",
"VIEW_15S",
"VIEW_3S_100PCT",
"VIEW_6S",
"WEBSITE_CONVERSIONS",
"WEBSITE_CONVERSIONS_V2"
],
"type": "string",
"description": "requires bidAmount too"
},
"name": {
"type": "string",
"description": "new line item (ad group) name"
},
"endTime": {
"type": "string",
"description": "ISO 8601 end time for this line item"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"bidAmount": {
"type": "number",
"description": "in the ad account's currency"
},
"startTime": {
"type": "string",
"description": "ISO 8601"
},
"lineItemId": {
"type": "string",
"description": "from list_x_ads_line_items"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"bidStrategy": {
"enum": [
"AUTO",
"GUARANTEED",
"MAX",
"TARGET"
],
"type": "string",
"description": "AUTO, GUARANTEED, MAX or TARGET; MAX and TARGET need bidAmount"
},
"dailyBudget": {
"type": "number",
"description": "only if the campaign is not budget-optimised"
},
"totalBudget": {
"type": "number",
"description": "only if the campaign is not budget-optimised"
},
"standardDelivery": {
"type": "boolean",
"description": "false = accelerated, only with line item budgets; X refuses it under a campaign budget"
}
}
}| getUploadUrl | boolean | ASK FOR A ONE-TIME UPLOAD URL instead of uploading now — use this whenever the file is on the user’s machine and you can run a shell or an HTTP request. Returns a uploadUrl you PUT the raw bytes to (any HTTP client), which answers with the durable Hermoso url. It beats `dataUri` for anything but a small image: a data: URI spends the whole file as tokens in this conversation. One file per url, and it expires. |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"url": {
"type": "string",
"description": "a PUBLIC http(s) URL Hermoso fetches server-side (private/internal addresses are refused, and every redirect hop is re-checked). Works on every surface including the hosted connector, and the bytes never cross this connection — prefer this whenever the file is reachable on the web."
},
"name": {
"type": "string",
"description": "original file name — helps pick the right extension"
},
"dataUri": {
"type": "string",
"description": "base64 data: URI of the file bytes (data:<mime>;base64,<…>) — bytes travel over this connection, so keep it small"
},
"getUploadUrl": {
"type": "boolean",
"description": "ASK FOR A ONE-TIME UPLOAD URL instead of uploading now — use this whenever the file is on the user’s machine and you can run a shell or an HTTP request. Returns a uploadUrl you PUT the raw bytes to (any HTTP client), which answers with the durable Hermoso url. It beats `dataUri` for anything but a small image: a data: URI spends the whole file as tokens in this conversation. One file per url, and it expires."
}
}
}| string |
| A YouTube video id for a VIDEO asset |
| loginCustomerId | string | manager id if operating through an MCC |
| assetResourceName | string | with appAdId: add a playable already in the asset library instead of uploading one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "asset name"
},
"appAdId": {
"type": "string",
"description": "also add the playable to this App ad (App campaigns for installs only)"
},
"imageUrl": {
"type": "string",
"description": "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."
},
"customerId": {
"type": "string",
"description": "10-digit account id (dashes ok) — omit to use the brand’s selected default account"
},
"playableUrl": {
"type": "string",
"description": "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"
},
"youtubeVideoId": {
"type": "string",
"description": "A YouTube video id for a VIDEO asset"
},
"loginCustomerId": {
"type": "string",
"description": "manager id if operating through an MCC"
},
"assetResourceName": {
"type": "string",
"description": "with appAdId: add a playable already in the asset library instead of uploading one"
}
}
}| channelId | string | your own YouTube channel (UC…), else Google's |
| customerId | string | Google Ads account id, 10 digits (dashes ok); omit for the brand's default account |
| 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 |
| description | string | YouTube description for the uploaded video (optional) |
| waitSeconds | number | seconds to wait for YouTube to finish processing, max 240; default 90 on upload, 0 on status |
| loginCustomerId | string | manager (MCC) account id, when the account is managed through one |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"title": {
"type": "string",
"description": "required, ≤100 chars"
},
"video": {
"type": "string",
"description": "Hermoso render id, Library id, attachment://N or Hermoso video link"
},
"action": {
"enum": [
"upload",
"status",
"list",
"remove"
],
"type": "string",
"description": "default upload"
},
"dryRun": {
"type": "boolean",
"description": "true = build and return the request without uploading any bytes"
},
"confirm": {
"type": "boolean",
"description": "true to remove the upload (action remove); every ad using the video stops showing it"
},
"privacy": {
"enum": [
"UNLISTED",
"PUBLIC"
],
"type": "string",
"description": "UNLISTED (default, the only option on Google's channel) or PUBLIC (own channel only, with channelId)"
},
"uploadId": {
"type": "string",
"description": "upload id from the upload reply or action list; required for status and remove"
},
"channelId": {
"type": "string",
"description": "your own YouTube channel (UC…), else Google's"
},
"customerId": {
"type": "string",
"description": "Google Ads account id, 10 digits (dashes ok); omit for the brand's default account"
},
"aiGenerated": {
"type": "boolean",
"description": "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"
},
"description": {
"type": "string",
"description": "YouTube description for the uploaded video (optional)"
},
"waitSeconds": {
"type": "number",
"description": "seconds to wait for YouTube to finish processing, max 240; default 90 on upload, 0 on status"
},
"loginCustomerId": {
"type": "string",
"description": "manager (MCC) account id, when the account is managed through one"
}
}
}| label in Snapchat Ads Manager |
| addUtm | boolean | append utm_source=snapchat, utm_medium=paid_social, utm_campaign={{campaign.name}}, utm_content={{ad.id}} (utm overrides any of them). Nothing is added unless asked |
| mediaId | string | build a creative on media ALREADY uploaded to this ad account instead of uploading again — the way to reuse one video across several creatives |
| creative | boolean | set false to upload the MEDIA ONLY and build the creative yourself — the default true is what an ad actually needs |
| headline* | string | REQUIRED by Snapchat. MAX 34 CHARACTERS — the text shown beneath the brand name |
| brandName | string | MAX 32 CHARACTERS |
| profileId | string | older name for snapProfileId (MCP only) |
| adAccountId | string | Snapchat ad account id shared with this profile; omit when only one is shared |
| callToAction | string | the swipe-up button label; must fit the creative type |
| creativeType | string | omit it: LEAD_GENERATION when leadGenerationFormId is given, WEB_VIEW when landingPageUrl is given, otherwise SNAP_AD (a top snap only) |
| snapProfileId | string | REQUIRED (or profileId): the Snapchat Public Profile the ad posts as. Snapchat has rejected every creative without one since 2024-02-26. From list_snapchat_ads_profiles, or Snapchat Ads Manager. |
| landingPageUrl | string | where a swipe-up goes |
| leadGenerationFormId | string | build a LEAD_GENERATION creative on this lead form (list_snapchat_ads_lead_forms / create_snapchat_ads_lead_form) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"headline"
],
"properties": {
"url": {
"type": "string",
"description": "public https url of the render — a Hermoso render URL already is one; for any other file run it through upload_file first. Omit only when reusing mediaId."
},
"utm": {
"type": "object",
"properties": {
"term": {
"type": "string",
"description": "utm_term value appended to landingPageUrl"
},
"medium": {
"type": "string",
"description": "utm_medium value appended to landingPageUrl"
},
"source": {
"type": "string",
"description": "utm_source value appended to landingPageUrl"
},
"content": {
"type": "string",
"description": "utm_content value appended to landingPageUrl"
},
"campaign": {
"type": "string",
"description": "utm_campaign value appended to landingPageUrl; Snapchat macros such as {{campaign.name}} stay literal"
}
},
"description": "UTM parameters appended to landingPageUrl. Snapchat URL macros such as {{campaign.name}}, {{adSet.name}}, {{ad.id}} are kept literal so Snapchat fills them in"
},
"kind": {
"enum": [
"video",
"image"
],
"type": "string",
"description": "default video"
},
"name": {
"type": "string",
"description": "label in Snapchat Ads Manager"
},
"addUtm": {
"type": "boolean",
"description": "append utm_source=snapchat, utm_medium=paid_social, utm_campaign={{campaign.name}}, utm_content={{ad.id}} (utm overrides any of them). Nothing is added unless asked"
},
"mediaId": {
"type": "string",
"description": "build a creative on media ALREADY uploaded to this ad account instead of uploading again — the way to reuse one video across several creatives"
},
"creative": {
"type": "boolean",
"description": "set false to upload the MEDIA ONLY and build the creative yourself — the default true is what an ad actually needs"
},
"headline": {
"type": "string",
"description": "REQUIRED by Snapchat. MAX 34 CHARACTERS — the text shown beneath the brand name"
},
"brandName": {
"type": "string",
"description": "MAX 32 CHARACTERS"
},
"profileId": {
"type": "string",
"description": "older name for snapProfileId (MCP only)"
},
"adAccountId": {
"type": "string",
"description": "Snapchat ad account id shared with this profile; omit when only one is shared"
},
"callToAction": {
"type": "string",
"description": "the swipe-up button label; must fit the creative type"
},
"creativeType": {
"type": "string",
"description": "omit it: LEAD_GENERATION when leadGenerationFormId is given, WEB_VIEW when landingPageUrl is given, otherwise SNAP_AD (a top snap only)"
},
"snapProfileId": {
"type": "string",
"description": "REQUIRED (or profileId): the Snapchat Public Profile the ad posts as. Snapchat has rejected every creative without one since 2024-02-26. From list_snapchat_ads_profiles, or Snapchat Ads Manager."
},
"landingPageUrl": {
"type": "string",
"description": "where a swipe-up goes"
},
"leadGenerationFormId": {
"type": "string",
"description": "build a LEAD_GENERATION creative on this lead form (list_snapchat_ads_lead_forms / create_snapchat_ads_lead_form)"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"url"
],
"properties": {
"url": {
"type": "string",
"description": "public https url of the file. A Hermoso render url works as-is; for a local or external file run upload_file first to get one."
},
"kind": {
"enum": [
"video",
"image"
],
"type": "string",
"description": "default video"
},
"fileName": {
"type": "string",
"description": "the label in TikTok Ads Manager. Omit it and a unique one is derived — TikTok rejects a DUPLICATE name on the same advertiser, so an explicit name that has been used before is refused."
},
"advertiserId": {
"type": "string",
"description": "from list_tiktok_ads_accounts — omit only when exactly one is reachable"
}
}
}| string |
| product colour |
| price | object | price object: {value in major units or amountMicros, currencyCode} |
| title | string | product title; Google needs it for the product to serve |
| gender | string | male, female or unisex |
| offerId* | string | your own unique id for the product — IMMUTABLE, and what you update or delete it by later |
| ageGroup | string | newborn, infant, toddler, kids or adult |
| material | string | product material |
| condition | string | new | refurbished | used |
| feedLabel | string | market bucket, e.g. "US" — taken from the feed when the feed declares one |
| imageLink | string | main product image URL (https); Google needs it for the product to serve |
| salePrice | object | sale price, same shape as price |
| attributes | object | any other product attribute by its Merchant API name — there are 145 and only the common ones are named above |
| 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) |
| description | string | product description; Google needs it for the product to serve |
| itemGroupId | string | shared id that groups the variants of one product, e.g. colours or sizes |
| availability | string | in_stock | out_of_stock | preorder | backorder |
| productTypes | array | merchant-defined category labels for the product, as a list |
| 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 |
| customAttributes | array | extra attributes as a list of {name, value} objects |
| identifierExists | boolean | false for a product with no GTIN/MPN/brand, such as a service or a bundle |
| merchantCenterId* | string | from list_merchant_accounts |
| additionalImageLinks | array | extra product image URLs, as a list |
| googleProductCategory | string | Google product taxonomy category |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"merchantCenterId",
"offerId"
],
"properties": {
"mpn": {
"type": "string",
"description": "manufacturer part number"
},
"gtin": {
"type": "string",
"description": "GTIN barcode (UPC, EAN or similar)"
},
"link": {
"type": "string",
"description": "the product page URL on the merchant's store"
},
"size": {
"type": "string",
"description": "product size"
},
"brand": {
"type": "string",
"description": "brand name"
},
"color": {
"type": "string",
"description": "product colour"
},
"price": {
"type": "object",
"required": [
"currencyCode"
],
"properties": {
"value": {
"type": "number",
"description": "major units, e.g. 19.99"
},
"amountMicros": {
"type": "string",
"description": "micros, if you already have them — 1 unit = 1000000"
},
"currencyCode": {
"type": "string",
"description": "three-letter ISO 4217, e.g. USD"
}
},
"description": "price object: {value in major units or amountMicros, currencyCode}"
},
"title": {
"type": "string",
"description": "product title; Google needs it for the product to serve"
},
"gender": {
"type": "string",
"description": "male, female or unisex"
},
"offerId": {
"type": "string",
"description": "your own unique id for the product — IMMUTABLE, and what you update or delete it by later"
},
"ageGroup": {
"type": "string",
"description": "newborn, infant, toddler, kids or adult"
},
"material": {
"type": "string",
"description": "product material"
},
"condition": {
"type": "string",
"description": "new | refurbished | used"
},
"feedLabel": {
"type": "string",
"description": "market bucket, e.g. \"US\" — taken from the feed when the feed declares one"
},
"imageLink": {
"type": "string",
"description": "main product image URL (https); Google needs it for the product to serve"
},
"salePrice": {
"type": "object",
"required": [
"currencyCode"
],
"properties": {
"value": {
"type": "number",
"description": "major units, e.g. 19.99"
},
"amountMicros": {
"type": "string",
"description": "micros, if you already have them — 1 unit = 1000000"
},
"currencyCode": {
"type": "string",
"description": "three-letter ISO 4217, e.g. USD"
}
},
"description": "sale price, same shape as price"
},
"attributes": {
"type": "object",
"description": "any other product attribute by its Merchant API name — there are 145 and only the common ones are named above",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"dataSource": {
"type": "string",
"description": "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)"
},
"description": {
"type": "string",
"description": "product description; Google needs it for the product to serve"
},
"itemGroupId": {
"type": "string",
"description": "shared id that groups the variants of one product, e.g. colours or sizes"
},
"availability": {
"type": "string",
"description": "in_stock | out_of_stock | preorder | backorder"
},
"productTypes": {
"type": "array",
"items": {
"type": "string"
},
"description": "merchant-defined category labels for the product, as a list"
},
"dataSourceName": {
"type": "string",
"description": "display name for a feed created on the fly"
},
"contentLanguage": {
"type": "string",
"description": "two-letter, e.g. \"en\" — taken from the feed when the feed declares one"
},
"customAttributes": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"value"
],
"properties": {
"name": {
"type": "string",
"description": "attribute name"
},
"value": {
"type": "string",
"description": "attribute value"
}
}
},
"description": "extra attributes as a list of {name, value} objects"
},
"identifierExists": {
"type": "boolean",
"description": "false for a product with no GTIN/MPN/brand, such as a service or a bundle"
},
"merchantCenterId": {
"type": "string",
"description": "from list_merchant_accounts"
},
"additionalImageLinks": {
"type": "array",
"items": {
"type": "string"
},
"description": "extra product image URLs, as a list"
},
"googleProductCategory": {
"type": "string",
"description": "Google product taxonomy category"
}
}
}| catalogId |
| string |
| write into a specific catalog instead of the store default |
| merchantId | string | Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store |
| adAccountId | string | Microsoft ad account id — omit to use the single shared account |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"dryRun": {
"type": "boolean",
"description": "validate against Microsoft and write nothing — Microsoft returns no ids for a dry run, so none is claimed"
},
"product": {
"type": "object",
"description": "ONE whole offer — availability, channel, condition, contentLanguage, imageLink, link, offerId, price{value,currency}, targetCountry, title are required; brand/gtin/mpn strongly recommended",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"products": {
"type": "array",
"items": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"description": "several whole offers in one batch, maximum 300"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"catalogId": {
"type": "string",
"description": "write into a specific catalog instead of the store default"
},
"merchantId": {
"type": "string",
"description": "Microsoft Merchant Center store id from list_microsoft_merchant_stores — omit when the account has exactly one store"
},
"adAccountId": {
"type": "string",
"description": "Microsoft ad account id — omit to use the single shared account"
}
}
}| string |
| Vercel's OData filter, e.g. utmSource eq 'facebook' |
| eventName | string | only this event, e.g. 'Signup' |
| projectId* | string | project id or name (list_vercel_projects) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"projectId"
],
"properties": {
"by": {
"type": "string",
"description": "one or TWO comma-separated dimensions: eventName, eventData, eventData/<property>, utmSource, utmMedium, utmCampaign, utmContent, utmTerm, requestPath, referrerHostname, country, deviceType, osName, browserName, route, environment, flags, flags/<key>, or one time step hour/day/week/month/year"
},
"days": {
"type": "number",
"description": "1-366, default 30"
},
"limit": {
"type": "number",
"description": "rows in the breakdown, 1-100, default 10"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD (instead of days)"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD (default now)"
},
"filter": {
"type": "string",
"description": "Vercel's OData filter, e.g. utmSource eq 'facebook'"
},
"eventName": {
"type": "string",
"description": "only this event, e.g. 'Signup'"
},
"projectId": {
"type": "string",
"description": "project id or name (list_vercel_projects)"
}
}
}| projectId* |
| string |
| project id or name (list_vercel_projects) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"projectId"
],
"properties": {
"by": {
"type": "string",
"description": "one or TWO comma-separated dimensions (at most one time step): requestPath, referrerHostname, country, deviceType, osName, browserName, route, utmSource, utmMedium, utmCampaign, utmContent, utmTerm, environment, flags, flags/<key>, hour, day, week, month, year — e.g. 'day,utmCampaign'"
},
"days": {
"type": "number",
"description": "1-366, default 30"
},
"limit": {
"type": "number",
"description": "rows in the breakdown, 1-100, default 10"
},
"since": {
"type": "string",
"description": "YYYY-MM-DD (instead of days)"
},
"until": {
"type": "string",
"description": "YYYY-MM-DD (default now)"
},
"filter": {
"type": "string",
"description": "Vercel's OData filter, e.g. utmSource eq 'facebook'"
},
"projectId": {
"type": "string",
"description": "project id or name (list_vercel_projects)"
}
}
}| default TOTAL |
| metricGroups | array | default ENGAGEMENT |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"entityIds"
],
"properties": {
"entity": {
"enum": [
"ACCOUNT",
"CAMPAIGN",
"FUNDING_INSTRUMENT",
"LINE_ITEM",
"MEDIA_CREATIVE",
"ORGANIC_TWEET",
"PROMOTED_ACCOUNT",
"PROMOTED_TWEET"
],
"type": "string",
"description": "default CAMPAIGN"
},
"endTime": {
"type": "string",
"description": "ISO 8601; defaults to now"
},
"accountId": {
"type": "string",
"description": "older name for adAccountId (MCP only)"
},
"entityIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "max 20"
},
"placement": {
"enum": [
"ALL_ON_TWITTER",
"PUBLISHER_NETWORK",
"SPOTLIGHT",
"TREND"
],
"type": "string",
"description": "default ALL_ON_TWITTER"
},
"startTime": {
"type": "string",
"description": "ISO 8601; defaults to 7 days before endTime"
},
"adAccountId": {
"type": "string",
"description": "X ad account id (list_x_ads_accounts); omit if only one is shared"
},
"granularity": {
"enum": [
"HOUR",
"DAY",
"TOTAL"
],
"type": "string",
"description": "default TOTAL"
},
"metricGroups": {
"type": "array",
"items": {
"enum": [
"BILLING",
"ENGAGEMENT",
"LIFE_TIME_VALUE_MOBILE_CONVERSION",
"MEDIA",
"MOBILE_CONVERSION",
"VIDEO",
"WEB_CONVERSION"
],
"type": "string"
},
"description": "default ENGAGEMENT"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"kind"
],
"properties": {
"kind": {
"enum": [
"INTEREST",
"CONVERSATION",
"PLATFORM",
"DEVICE",
"LANGUAGE",
"APP_STORE_CATEGORY",
"NETWORK_OPERATOR",
"TV_MARKET",
"TV_SHOW",
"EVENT"
],
"type": "string",
"description": "targeting vocabulary to search, e.g. INTEREST, DEVICE, LANGUAGE or EVENT"
},
"limit": {
"type": "number",
"description": "maximum number of results to return"
},
"query": {
"type": "string",
"description": "filter by name"
},
"locale": {
"type": "string",
"description": "TV_SHOW — required; get one from kind:\"TV_MARKET\""
},
"osType": {
"enum": [
"ANDROID",
"IOS"
],
"type": "string",
"description": "APP_STORE_CATEGORY"
},
"eventTypes": {
"enum": [
"CONFERENCE",
"HOLIDAY",
"MOVIE_RELEASE",
"MUSIC_AND_ENTERTAINMENT",
"OLYMPICS",
"OTHER",
"POLITICS",
"RECURRING",
"SPORTS"
],
"type": "string",
"description": "EVENT"
},
"countryCode": {
"type": "string",
"description": "NETWORK_OPERATOR, e.g. US"
}
}
}