Documentation

MCP, CLI & API

New account-wide reads, insights, editing, copying and history tools are in beta and are turned on per account; existing commands and parent-scoped campaign/ad set/ad reads remain available.

CLI Full Reference

This is the complete reference for the Ads Uploader CLI. For an introduction and setup guide, see CLI Configuration.

History

ads history shows the same launches and changes as the Ads Uploader History page, newest first: Uploader, Duplicator, AI Edits, CLI, MCP and API activity. Use ads history launch <launchId> for campaigns, ad sets, ads, results and failure messages from one launch.

ads history --account act_123 --days 30
ads history --type changes --source mcp --search budget
ads history launch <launchId> --json

Both commands use --account or your default account. List filters are --days 7|30|90 (default 30; History covers the last 90 days), --type all|launches|changes (default all), --source web|duplicator|mcp|cli|api, --search <text> (up to 200 characters), and --member <id> (a team member ID). Use --limit 1..100 (default 50) and the printed --before <cursor> hint for more. Keep the same filters when paging. Tables show Time, Item, Type, Change, Before, After, Source and Status, plus By for teams. --json returns the full response unchanged, including paging and pending suggestions. The History page is open to everyone; reading History through the API, CLI and MCP is in beta.

ads meta-activity reads Meta's ad account activity log, including edits in Ads Manager and other tools. It retains --since, --until, --object, --after, --limit, --fresh, --account and --json. The default range is 7 days, with a maximum of 90 days.

ads history --meta and ads history with --since, --until or --object still read Meta activity for one release and print a deprecation note to stderr. Switch to ads meta-activity. The ads edits list is also deprecated; use ads history --type changes. ads edit:show, undo and ads edits cancel keep working.

Agent Inbox history

ads inbox:history (also ads inbox history) reads your retained suggestions and direct edit outcomes from the last 90 days, newest first, without calling Meta. It includes agent notes, edited-note markers, run titles, client names, before/after values and outcomes. Read permission is required; hidden and Read-off accounts are excluded.

ads inbox:history --account act_123 --campaign 12345 --status applied --json
ads inbox history --account act_123 --since 2026-10-01 --until 2026-10-09
ads inbox:history --run <reviewId> --kind duplicate --limit 20

Use --account or your selected account; with neither, history covers all permitted accounts. Filters are --campaign, --since/--until (inclusive ISO dates or timestamps with a timezone), --run <reviewId>, --status waiting|applied|failed|cancelled|skipped|stale, --kind edit|duplicate, and --action <operation> such as set_bid, rename or duplicate. A date-only --until includes the whole UTC day. Waiting includes applying, and failed includes partial outcomes; outcome preserves the specific result. Time is resolution time when recorded, otherwise creation time. Direct edits can be filtered by campaign only when that campaign ID was recorded. Use --limit 1..100 (default 50) and --cursor from the previous response, keeping the same filters. --json returns the full response including paging.cursor. The table's revised column shows the latest change made to a suggestion after the agent made it, such as user restored agent value or user edited note; --json includes every entry in revisions with at, by (user, or agent with source and clientName), kind (value or note), action (edited or restored_to_agent), field, before and after.

Commands

Authentication

CommandWhat it does
ads loginBrowser login, with automatic device login on servers
ads login --deviceApprove sign-in on another device
ads login --browserForce the localhost browser flow
ads login --api-key [key]Verify and save an API key; omit the value to paste or pipe it
ads logoutClear stored credentials
ads whoamiShow the signed-in email, default ad account and API URL
ads configShow whether you are signed in, your email, default ad account, API URL and config folder (~/.config/adsuploader/)
ads --versionShow the installed CLI version

A login token lasts 30 days. After that, run ads login again.

Logging in on a server (no browser)

On a VPS, in SSH, or on a machine without a browser, run:

ads login --device

Open the printed URL on your phone or laptop, enter the code, and approve the device after checking its country and network. The code expires after 10 minutes. The CLI saves a 30-day login on the server. ads login automatically uses this flow over SSH or on Linux without a display, and falls back to it if opening the browser fails. Use ads login --browser to force the localhost browser flow.

API Keys for Your Own Agents and Scripts

See API Keys for the full setup and one-person use rules. For your own agents and scripts, open Account > MCP, CLI & API, choose Create API Key, and give it a name. Copy the adk_ secret when it appears; it is shown only once. You can have 1 active key by default. Add a team seat for another person, or contact support if you need more keys. Keys can be used from different networks and require CLI access on your account. Keys do not expire. Keys you don't use for 90 days are revoked, and we email you when that happens. Time when your plan is paused doesn't count.

Set ADS_API_KEY through your server or CI secret settings. It overrides the token saved in ~/.config/adsuploader/credentials.json. To save a key locally instead, run:

ads login --api-key

Paste the key at the hidden prompt, or pipe it through stdin. The CLI verifies it before saving it. ads login --api-key <key> also works, but putting a secret in the command can expose it in shell history. ads whoami and ads config show the authentication method; only the key prefix is displayed.

Choose Revoke in API Keys under Account > MCP, CLI & API when a key is no longer needed. ads logout removes a saved API key from this machine without revoking it. It cannot unset ADS_API_KEY; remove that variable from your environment or secret settings. Browser and device sessions can be revoked under MCP and CLI-Connected AI Apps; logging out of a session also attempts to revoke it.

Browsing

CommandWhat it does
ads accountsList all ad accounts connected to your Meta account
ads accounts:refreshFetch the ad account list from Meta again right away, for example after you get access to a new ad account
ads account <id>Set a default ad account for future commands
ads pagesList Facebook Pages you can advertise with, including any linked Instagram account, for use with profile overrides
ads targeting:search "Austin" --type cityFind city keys for ad set targeting
ads targeting:search "90210" --type zipFind zip / post code keys for ad set targeting
ads targeting:search "advertising" --type detailedFind detailed-targeting IDs and types
ads campaignsList active campaigns
ads campaigns --status allInclude inactive campaigns as well
ads campaigns --search "text"Filter campaigns by name
ads campaign <id>Show the ad sets inside a campaign
ads adsets --campaign <id>List ad sets in a campaign (supports --search, --status)
ads adsets --search "text"Find ad sets by name across the whole account, with each ad set's campaign
ads adset <id>Show the ads inside an ad set
ads ads --search "text"Find ads by name across the whole account, with each ad's ad set and campaign. Add --adset <id> to list one ad set
ads ad <id>View full ad details including creative settings and the ad's Facebook and Instagram post IDs and links (effectiveObjectStoryId, effectiveInstagramMediaId, facebookPostUrl, instagramPermalinkUrl). A disapproved ad or one with issues also shows Meta's reasons
ads insightsQuery one bounded page of Meta Ads Insights
ads insights:job <jobId>Check or page an async Insights report
ads insights:fieldsList supported Insights fields and query options
ads presetsList your saved API presets
ads presets <id>Show details for a specific preset
ads presets:save --from-ad <adId> --name "Preset Name"Save an existing ad as an API preset. Add --share to share it with your team (team plans).
ads text-presetsList your saved text presets
ads text-presets <id>Show details for a specific text preset
ads uploadsList recent upload batches (20 by default; change with --limit <n>)
ads uploads <batchId>Show batch details (files, variants, hashes)

Saved Builds

Saved builds are the same builds you see in the web uploader's Saved Builds window. See Saved Builds for how they work.

CommandWhat it does
ads buildsList your saved builds (filter with --account <id>)
ads builds <buildId>Show one saved build, including its current revision number
ads builds:create --spec spec.json --name "Summer Sale"Save a spec as a new build. builds:save is an alias. Also takes --notes, --account and --web-state <file>.
ads builds:update <buildId> --spec-patch patch.json --expected-revision <n>Change part of a build's spec with a JSON merge patch. --expected-revision is required and stops you from overwriting someone else's newer edit.
ads builds:update <buildId> --name "New name"Rename a build. --notes, --spec <file> (replace the whole spec) and --web-state <file> also work.
ads builds:fork <buildId>Copy a build into a new draft, leaving the original unchanged
ads builds:delete <buildId>Delete a saved build
ads launch:discard <buildId>Discard a launch waiting in your Agent Inbox; the saved build stays

Pass - instead of a file name to read the JSON from stdin. To launch a build, use ads create --build <buildId> (or create:preview).

Media Upload

CommandWhat it does
ads upload <inputs...>Upload local paths and public HTTPS URLs to your ad account
ads upload ./directory/Upload an entire directory
ads upload:drive <folderUrl>Import a public Google Drive folder as a background job (takes --account, --json and --api-timeout)
ads upload --retry-failed [batchId]Retry the failed files from your last failed upload batch, or from the batch you name. Videos Meta already received pick up where they left off instead of uploading again. Do not pass files with this flag.

HTTPS arguments, including public Google Drive file links, are detected automatically. You can mix local paths and URLs: local files are uploaded first, then the server imports URLs into the same batch so grouping works across every input. Public Drive folders must be shared as Anyone with the link (Viewer).

Local files are staged in parallel, and transient network failures are retried automatically with backoff. URL and Drive-folder imports use bounded parallel file pipelines in a background job while the CLI displays a completed/total counter and every active file. You rarely need to touch these flags, but they're available:

Upload flagDescription
--concurrency <n>Number of files staged in parallel, 1-6 (default: 4). Large videos are throttled automatically to stay within memory.
--upload-timeout <ms>Per-file upload timeout (default: 120000)
--api-timeout <ms>API request timeout in milliseconds (default: 60000). Also on upload:drive. You can set it for every command with the ADS_API_TIMEOUT_MS environment variable.

Upload rules:

  • Each file can be up to 4 GB.
  • Images: .jpg, .jpeg, .png, .gif, .bmp, .webp. Videos: .mp4, .mov, .avi, .mkv, .webm, .m4v.
  • Links must use https://. Anything else is treated as a local path.
  • An image named like its video plus _thumbnail (for example promo_thumbnail.jpg for promo.mp4) is attached to that video as its custom thumbnail instead of being uploaded as separate media.
  • Pressing Ctrl-C during a link or Drive import asks the server to stop the import. Files that already finished stay in the batch.

Ad Creation

CommandWhat it does
ads create spec.jsonCreate ads from a spec file
ads create:preview spec.jsonDry run showing what would be created
ads create:interactiveGuided wizard (accepts all create flags)

Post-ID Duplication

CommandWhat it does
ads duplicator:post-id [specFile]Duplicate existing ads selected by Page post ID while preserving their post references
ads duplicator:post-id:preview [specFile]Preview a post-ID duplication and its source-to-destination mapping
ads duplicator:copy:preview [specFile]Preview copying campaigns, ad sets or ads by ID (tree, skipped objects, overrides, warnings, call counts)
ads duplicator:copy [specFile]Copy campaigns, ad sets or ads by ID after preview; Agent Launch Approval sends it for approval
ads duplicator:status <jobId>Show a duplication job's progress and its created, failed and skipped objects

Job Management

CommandWhat it does
ads jobs <jobId>Check the status of a job
ads jobs <jobId> --followStream live progress updates
ads jobs cancel <jobId>Cancel a running job

Resource Read Flags

CommandWhat it reads
ads account:info <id>Owned account status, currency, timezone, spend and funding display
ads insights:health --campaign <id> --preset last_30dCampaign performance, native results, status, budget, issues and learning counts
ads audiences --type all --search "Customers"Custom, lookalike and saved audiences
ads conversionsPixels, custom conversion definitions and standard event names
ads estimate --targeting targeting.json --goal REACHPredicted audience size and delivery curve
ads meta-activity --since 2026-08-01 --until 2026-08-07Meta change history
ads library:images / ads library:videosMedia in the Meta ad account library
ads posts --page <id> / ads posts --instagram <id>Accessible Page promotable posts or connected Instagram media

Every command supports --json and --fresh. List commands accept --limit and --after. The existing campaigns, campaign, adsets and adset commands default to 100 rows; new list commands default to 25 (maximum 100); health is capped at 25 campaigns. New resource commands return the full JSON response, including paging.after, paging.hasMore and cache metadata. campaigns --json, adsets --json and ads --json return arrays; campaign <id> --json and adset <id> --json retain their ID-and-list envelopes. These five commands print next-page hints to stderr in JSON mode, keeping stdout compatible with existing scripts. Keep filters unchanged when continuing. These reads make one request attempt; honor the retry wait on a 429.

Without --campaign, ads adsets lists ad sets across the whole account; without --adset, ads ads lists ads across the account. Add --search to find ad sets or ads by name when you don't know their campaign or ad set. Each row names its campaign by ID and name, and ads also name their ad set. These return one page (adsets defaults to 100 rows, ads to 25) and never page through the account on their own; use the printed --after hint for more. The default --status active leaves out paused items, so add --status all to include them.

ads account <id> selects the default account; ads account:info <id> reads details. Use --account on scoped commands to override the default. Campaign tables show real status, objective and budgets. Ad set tables show status, optimization goal and budgets; ad tables show status and creative IDs because ads have no independent budget.

Health answers “how is this campaign doing”; use insights for custom analysis. Results preserve each native indicator, value and attribution window. Missing delivery metrics are null. Learning counts cover at most 100 ad sets per campaign, with an explicit partial warning. Meta change history is separate from the Ads Uploader Activity Log, and the Meta library is separate from Ads Uploader uploads.

CommandAdditional flags
insights:health--campaign, --preset, or both --since and --until
audiences--type custom|lookalike|saved|all, --search
estimateRequired --targeting <file.json> and --goal <GOAL>; optional --promoted-object <file.json>
meta-activity--since, --until, --object; last 7 days by default, maximum 90 days
postsExactly one of --page or --instagram
campaigns, campaign, adsets, adset, ads--search, --status <value> (legacy lists filter exact active; other strings mean all), --limit, --after, --fresh; adsets also takes --campaign and ads takes --adset

The estimate file contains the targeting object itself, for example {"countries":["US"],"ageMin":21,"genders":"women"}. It uses the ad-creation targeting builder and needs at least one location. Estimates also accept customAudiences and excludedCustomAudiences as numeric ID strings. Add a promoted object when the chosen goal requires one. Predictions do not guarantee delivery.

Create Flags

These flags apply to ads create, ads create:preview and ads create:interactive. You can use them instead of, or as well as, a spec file.

FlagDescription
--account <id>Override default ad account
--idempotency-key <key>Tie retries to one launch for 24 hours (8–128 letters, digits, underscores or hyphens). Sent on create only
--allow-repeatCreate the same ads again instead of returning the existing launch. Use only when the user really wants a repeat
--build <buildId>Use a saved build as the spec. Do not combine it with a spec file.
--preset <id>Use a saved API preset (alternative to spec file)
--text-preset <id>Load a saved text preset
--copy-from <adId>Copy settings from an existing ad
--upload <batchId>Specify the upload batch ID
--status <PAUSED|ACTIVE>Set ad status (default: ACTIVE)
--start-time <iso>Scheduled start: account-local YYYY-MM-DDTHH:mm[:ss], or ISO 8601 with an offset or Z (converted to the account time zone)
--end-time <iso>Scheduled end: account-local YYYY-MM-DDTHH:mm[:ss], or ISO 8601 with an offset or Z (converted to the account time zone); use at least 24 hours after the start for a lifetime budget
--pause-at <level>Pause level: ad (default), adSet, or campaign
--daily-budget <amount>Override daily budget per ad set (currency units, e.g. 50 for $50)
--bid-amount <amount>Override bid/cost cap per ad set (currency units)
--minimum-roas <ratio>Override the minimum ROAS goal per ad set (e.g. 1.5)
--campaign-daily-budget <amount>Set a CBO daily campaign budget in whole currency units. Mutually exclusive with the lifetime budget; omit both to inherit the source campaign's budget.
--campaign-lifetime-budget <amount>Set a CBO lifetime campaign budget in whole currency units. Mutually exclusive with the daily budget; omit both to inherit the source campaign's budget.
--adset-min-spend <amount>Set the ad-set minimum spend under CBO in whole currency units; 0 removes the limit inherited from the source ad set.
--adset-max-spend <amount>Set the ad-set maximum spend under CBO in whole currency units; 0 removes the limit inherited from the source ad set.
--adset-min-spend-pct <5-100>Set the ad-set minimum spend as a percentage of the campaign budget, in 5% steps. Also works with an existing CBO campaign; mutually exclusive with --adset-min-spend.
--adset-max-spend-pct <5-100>Set the ad-set maximum spend as a percentage of the campaign budget, in 5% steps. Also works with an existing CBO campaign; mutually exclusive with --adset-max-spend.
--location <ISO>Target a country by two-letter ISO code. Repeat the flag for multiple countries.
--age-min <n>Minimum age, from 13 to 65
--age-max <n>Maximum age, from 13 to 65 (65 means 65+)
--gender <gender>all, men, or women. all clears a gender restriction inherited from the source ad set.
--ai-disclosureSelf-disclose AI-generated creative content (Meta AI-content transparency). Off unless the copied ad already discloses. See AI disclosure for the spec field.
--page <id>Use this Facebook Page instead of the template's (see Profile Options)
--instagram <id>Use this Instagram account instead of the template's
--use-page-identityUse the Facebook Page as the Instagram identity; cannot be combined with --instagram
--threads <id>Use this Threads profile instead of the template's
--text-file <path>Load text configuration from a JSON file
--expandedShow full headline, primary text, and description values in previews

Copy by ID

duplicator:copy is the CLI version of the MCP ads_duplicate tool: the same as Ads Manager's Duplicate. Pick sources with --campaign, --adset or --ad <id> (repeatable, one level per request) or a spec file shaped like the v1 /duplications/copies body (level, ids, deepCopy, destination, rename, overrides, ...). Add --rename <pattern> to name the copies with {name}, {date} (UTC) and {index}. Copies include child objects by default; --deep-copy asks for that explicitly and --no-deep-copy copies only the selected roots. Always run duplicator:copy:preview first. Without a sourceFingerprint, duplicator:copy only previews (status: "requires_preview", exit 1) and nothing is copied; save resolvedRequest from --json as the spec and run it again. Pass --confirm-spend and --acknowledge-warnings only after the user approves. With Agent Launch set to Approval it prints the approval link and copies nothing until approved. Otherwise it polls the job for up to 30 minutes; if it is still running, resume with ads duplicator:status <jobId>.

Post-ID Duplication Flags

Agent duplications through MCP, CLI and the public API follow the account’s Agent Launch setting on the Account Permissions page, the same as launches. Agent Edit does not apply to them. Allowed (the default) starts the copy directly. Approval returns pending_approval and reviewUrl; nothing is duplicated until the user approves in your Agent Inbox. Share the link and do not retry or poll: there is no job until approval. Off refuses the request with AGENT_LAUNCH_DISABLED. Regular ACTIVE copies may require explicit confirmSpend: true (--confirm-spend in the CLI); post-ID copies never do. Uploads from new media and the human-operated web Duplicator are unaffected.

These flags apply to ads duplicator:post-id and ads duplicator:post-id:preview. They can be used instead of or alongside a post-ID duplication spec file. This is the post-ID duplication mode; other duplication modes may be added later.

FlagDescription
--account <id>Override the default ad account
--post <postId>Discover source ads by Page post ID. Multiple matches require an explicit --ad selection.
--ad <adId>Select an exact source ad ID. Repeat for multiple ads; do not combine with --post.
--campaign <id>Use an existing destination campaign
--adset <id>Use an existing destination ad set, or use it as the template for a new ad set
--new-adset [name]Create one new ad set for all source ads. The default name is {AdName}.
--new-adset-per-ad [name]Create one new ad set per source ad. The default name is {AdName}.
--ad-name <pattern>Set the duplicated ad name pattern. The default is {AdName}.
--pausedCreate duplicated ads paused
--confirm-spendAccepted for older scripts; post-ID copies never require it
--use-creative-idReuse the original creative IDs instead of creating post-reference creatives
--acknowledge-warningsContinue a live run after you have reviewed the creative warnings in the preview (acknowledgeWarnings: true in JSON)
--jsonOutput raw JSON for scripting

Post-ID Duplication Spec File

This v1 spec selects an exact source ad, clones one ad set and creates paused ads:

{
  "adIds": ["120200000000000001"],
  "campaignId": "120200000000000000",
  "adSetId": "120200000000000002",
  "newAdSet": { "name": "Winners {AdName}" },
  "adNamePattern": "{AdName} - {Index}",
  "paused": true,
  "useCreativeId": false
}

Use either postIds for discovery or ordered adIds for exact selection, never both. Preview returns sourceCandidates with ad/campaign/ad-set IDs. When matches are ambiguous, requiresSourceSelection is true and resolvedRequest is null: choose the ad IDs and preview again. With an exact selection, resolvedRequest contains ad IDs and no post IDs. An existing campaignId is required; new campaigns are unsupported.

Choose one ad set destination mode:

DestinationSpec fields
Existing ad set"adSetId": "120200000000000002"
One new ad set"newAdSet": { "name": "Winners {AdName}" } and adSetId as the template (required)
One new ad set per ad"newAdSetPerAd": { "name": "Winners {Index} {AdName}" } and adSetId as the template (required)

For live runs with creative warnings, review the preview and pass --acknowledge-warnings (acknowledgeWarnings: true in JSON/MCP) to continue. Explicit useCreativeId: true also satisfies this choice. Preview does not require acknowledgment.

adSetId is always required. New ad sets copy the budget, bid strategy, targeting and schedule of that ad set, never the source ad's own ad set, so a copy out of a campaign-budget campaign still gets a real ad set budget. The destination campaign, selected ad set and source ads must belong to the same ad account. A selected ad set must belong to campaignId, including when used as the template for a new set. Choose either newAdSet or newAdSetPerAd; combining both is rejected. One source with newAdSetPerAd creates one shared clone and keeps {Index} as 1.

Preview reads the real configuration of every source ad and selected template without creating Meta objects. Later sources can therefore reveal missing data and creative warnings; targeting is validated on each template used to create a new ad set. Simulated new ad sets use the destination campaign’s budget and bid settings when available. Ads default to active; paused affects ads only, and new ad sets remain active.

{AdName} inserts the source ad name. {Index} inserts its 1-based order in duplicated ad names and per-ad ad set names. When one new ad set contains multiple source ads, {AdName} in that ad set's name becomes Multiple Ads.

How post lookup picks ads: a post ID lookup searches up to about 2,000 recent ads in the account. It never picks the newest match for you. When several ads match, the CLI and MCP stop and ask you to choose, and they also stop on warnings you have not acknowledged. A live run always duplicates exact ad IDs, so submit the preview's resolvedRequest. To rerun a selection you already reviewed, save and reuse resolvedRequest (MCP also needs accountId) instead of repeating the post-only lookup. The ad IDs stay fixed, but the current Meta settings are read and checked again at launch.

New ad sets inherit targeting, budget, schedule and bid settings from the selected --adset or adSetId, with the same cleanup and destination-budget reconciliation as the web Duplicator. Without a selected template, a shared new set uses the first source ad’s set; per-ad mode uses each source ad’s own set.

Dynamic-creative source ads reuse their creative ID and cannot preserve synchronized engagement. The CLI shows the same warning as the web Duplicator.

Live post-ID duplication runs stream per-ad-set and per-ad progress, including Meta error messages, and finish with Duplicated X of Y.

Browse Flags

These flags are available on campaigns, adsets, ads, adset, and campaign:

FlagDescription
--status <status>active (default) or all
--inactiveShorthand for --status all (on campaigns)
--search <text>Filter by name (on campaigns, adsets, ads). Without --campaign or --adset, adsets and ads search the whole account

City, zip, and detailed targeting use Meta identifiers rather than names. Search through Ads Uploader to get values that can be pasted into a spec:

ads targeting:search "Austin" --type city
ads targeting:search "90210" --type zip
ads targeting:search "advertising" --type detailed
FlagDescription
--type <type>Required. city returns targeting.cities[].key values; zip returns targeting.zips[].key values; detailed returns entries for targeting.detailedTargetingGroups.
--account <id>Override the configured default ad account
--limit <n>Return 1 to 25 matches (default 8)
--jsonReturn pasteable structured results

Detailed-targeting results include id, name, type, an audience-size range, and the category path. The type identifies the detailed-targeting category, such as interests, behaviors, work_employers, or work_positions. Always search for these identifiers; never guess them.

Insights Reporting

ads insights reads one bounded page per selected ad account. It does not auto-fetch subsequent pages. ads insights:fields returns a compact group index and recommended fields. Use --search, --group, --kind, or --ids for full entries. Add --account to include custom conversions.

Insights query flags

FlagDescription
--mode <auto|sync|async>Execution mode, default auto
--freshRefresh the result instead of reading existing cached data
--no-waitPrint a submitted job and exit without polling
--account <id>Override the configured default ad account
--accounts <ids>2 to 5 comma-separated account IDs instead of --account; sync only
--compare <previous_period|previous_year>Prior values and changes beside each numeric metric; requires all_days and sync execution
--compare-range <since..until>Custom comparison in YYYY-MM-DD..YYYY-MM-DD format instead of --compare
--include <categories>Comma-separated status,budget,schedule,setup,targeting,creative
--level <level>account, campaign, adset, or ad
--fields <ids>Comma-separated native or friendly field IDs, such as purchases,roas
--action-types <types>Comma-separated action types to emit as explicit action columns
--action-breakdown <name>Repeatable action dimension; non-default values produce a separate action table and cannot use friendly aliases
--where <expression>Repeatable quoted filter, such as "spend>10" or "campaign.name~Summer"; up to 5
--exclude-zero-impressionsAdd an impressions > 0 filter
--format <objects|table>JSON row shape, default objects; table rows follow column order
--preset <preset>Meta date preset such as last_7d or last_30d
--since <YYYY-MM-DD>Explicit range start; requires --until and cannot be combined with --preset
--until <YYYY-MM-DD>Explicit range end; requires --since
--increment <value>all_days, daily, weekly, or monthly
--breakdown <name>Repeat at most twice. Asset, media, and hourly dimensions must each be used alone.
--campaign <id>Campaign ID filter. Repeat for multiple IDs.
--adset <id>Ad set ID filter. Repeat for multiple IDs.
--ad <id>Ad ID filter. Repeat for multiple IDs.
--attribution-windows <windows>Comma-separated attribution windows. Friendly metrics such as purchases follow one window; use native actions with --action-types to compare several
--report-time <time>conversion, impression, mixed, or lifetime
--use-account-attributionExplicitly use the ad account attribution setting
--sort <field:asc|desc>Sort one native scalar field
--limit <n>Return 1-100 rows
--after <cursor>Opaque cursor, or JSON account-to-cursor map with --accounts
--jsonPrint the raw response losslessly

Use --compare previous_period for the same number of days immediately before the current range, --compare previous_year for last year (Feb 29 clamps to Feb 28), or --compare-range 2026-08-01..2026-08-07 for a custom range. Comparisons require all_days, run synchronously, and cannot use maximum, data_maximum, or non-default action breakdowns. Each numeric metric gets .previous, .change, and .change_pct; missing matches and a zero prior denominator produce null changes or percentages. meta.compare records both ranges. COMPARE_PARTIAL means the bounded prior read did not cover all rows.

Use --include status,budget,schedule,setup,targeting,creative for entity context. Only requested categories are returned in separate entities.campaigns, entities.adsets, and entities.ads maps. Parent budgets use major currency units and retain raw <field>_minor strings. Targeting and dynamic creative assets are summaries. The table shows a short context column; --json preserves all metadata, linked to rows by meta.entityRefs. Entity metadata refreshes after 10 minutes independently of performance pages; failed metadata is null with ENTITY_METADATA_UNAVAILABLE.

Use --accounts act_123,act_456 instead of --account for 2 to 5 distinct accounts. Each runs sequentially with its own currency and timezone. Results lead with account_id and account_currency; meta.accounts records each account's cache, warnings, paging, and safe errors. Never add totals across accounts. Different currencies produce MIXED_CURRENCIES. Some account failures return successful rows with PARTIAL_RESULTS; retry failed accounts separately after any stated wait. If every account fails, the request fails, preferring 429 when throttled. Multi-account queries are synchronous, including when --compare is used; query heavy accounts separately.

Follow paging.byAccount with --after '{"act_123":"CURSOR","act_456":null}', keeping the other options unchanged. Use canonical act_ keys. Null marks an exhausted account; omitting a key starts that account's first page. --limit applies per account, within the shared 2,000-cell result bound. Comparison columns count toward that bound.

--where supports >, >=, <, <=, =, !=, and ~ (contains). Numeric metric values become numbers; names remain strings. Use REST or MCP for array-valued filters. Friendly formulas and custom conversion IDs are listed in Ads Insights. Use results,cost_per_result for campaign goals and asset breakdowns for flexible/dynamic creative ads.

Async reports and cache

For single-account queries without comparison, --mode auto is the default: daily ranges over 90 and up to 400 days, or Meta "too much data" responses, use async reports. --mode sync keeps the 90-day daily cap; --mode async always submits a report. The CLI follows pending jobs and writes progress to stderr. Use --no-wait to print the job ID and exit, or combine it with --json to preserve the pending response. Resume or page the report with:

ads insights:job ij_<id> --wait 20 --json
ads insights:job ij_<id> --after OPAQUE_CURSOR --limit 25 --json

insights:job also accepts --account and --format. It checks once, optionally waiting up to 20 seconds; job references last 24 hours. Follow the job ID instead of repeating the original query.

Results include meta.cache with hit, stored time, age in seconds, and lifetime. Cache lifetimes are 10 minutes for ranges including today, 15 minutes for ranges ending 1 to 3 days ago, 1 hour for 4 to 28 days ago, and 24 hours for older ranges. Completed job pages use a 1-hour cache. Table output shows the cache age. --fresh skips the existing query cache and refreshes it. A 429 INSIGHTS_ACCOUNT_BUSY means the account's concurrent request cap is full; respect Retry-After: 5. Queries and job reads never retry automatically.

Field catalog flags

FlagDescription
--account <id>Include custom conversion definitions and labels
--ids <ids>Comma-separated exact field IDs
--search <text>Search field IDs and labels
--kind <kind>dimension, metric, action_family, alias, breakdown, or date_preset
--group <group>Filter by catalog group
--recommendedReturn recommended fields only
--limit <n>Return 1-100 catalog entries
--offset <n>Start at a catalog offset
--jsonPrint the full catalog response as JSON

Reach and frequency are non-additive. Empty rows are a successful response because Meta omits zero-impression entities. Attribution defaults to the account setting unless windows are provided. If paging.hasMore is true, pass paging.after to --after only when another page is needed. There is no --all or CSV mode. A 429 exits non-zero and prints the retry wait without retrying the query.

Detail Flags

These flags are available on ad:

FlagDescription
--expandedShow full headline, primary text, and description values

Common Flags

FlagDescription
--account <id>Override default ad account for any command
--jsonOutput raw JSON (available on most commands, intended for scripting)

Environment Variables and Updates

VariableWhat it does
ADS_API_TIMEOUT_MSAPI request timeout in milliseconds for every command (default 60000)
ADS_API_URLThe Ads Uploader address the CLI talks to. Leave it unset for normal use.

The CLI checks for a new version once a day and prints a notice when one is out. Update with npm update -g @adsuploader/cli.

Spec File Format

The JSON spec file controls every aspect of ad creation. Provide a template source (adPresetId or copyFromAd) plus either uploadId or captured mediaItems with a Facebook mediaHash for every image and mediaId for every video. Standard, carousel, flexible, and placement videos use thumbnailHash; Multi Media videos use a public thumbnail URL. Missing library-video thumbnails are recovered from Meta during preview/create without re-uploading the video.

For videos, mediaItems[].mediaId must be the numeric Facebook video ID: use the upload response’s videoId, not its internal id. videoId is accepted as an alias, and linkedAssets[] follow the same rule. Internal upload IDs are resolved on save only when they belong to the authenticated user and selected account. Without a numeric ID or a batch link, an unresolved video makes the build unlaunchable. See the canonical placement-video item.

Minimal Spec

{
  "adPresetId": "your_preset_id",
  "campaign": { "name": "New Campaign" },
  "uploadId": "batch_abc123"
}

Full Example

{
  "adPresetId": "preset_id_here",
  "campaign": { "name": "New Campaign" },
  "uploadId": "batch_abc123",
  "adSet": {
    "name": "My New Ad Set",
    "dailyBudget": 50
  },
  "adNamePattern": "{filename}",
  "texts": {
    "perAd": {
      "hero.jpg": {
        "headlines": ["Main Headline"],
        "bodies": ["Ad copy here."],
        "descriptions": ["Short description"],
        "cta": "SHOP_NOW",
        "link": "https://example.com/landing"
      }
    }
  },
  "creativeEnhancements": "none",
  "options": {
    "status": "PAUSED",
    "pauseAt": "adSet"
  }
}

Template Source

You need one of these to tell the CLI which ad configuration to use as a base.

FieldDescription
adPresetIdA saved API preset ID supplying settings; requires an explicit campaign/ad-set destination.
copyFromAdA Facebook ad ID to copy settings from.

Presets reuse their saved campaign and ad set when no destination is supplied, including preset-only creates and older saved builds. Preview and create return a non-fatal PRESET_DESTINATION_USED warning identifying the saved destination. To choose a different destination, pass campaign.name for a new campaign, named campaign.campaigns entries in duplicate/split mode, or campaign.id / adSet.id for an existing destination. With only an explicit campaign.id, new ad sets are created. With copyFromAd and no campaign or adSet, a new paused campaign and ad set are created from the template ad's settings; the template's own campaign and ad set are never used unless you pass their IDs.

Cross-campaign create/preview is available only for direct agent requests. Saved builds and Agent Inbox launches must use the template ad's campaign or a new campaign; to save a build for another existing campaign, copy from an ad in that campaign (campaign.id + adSet.id picks one). Existing stored cross-campaign builds can still launch, but cannot be re-saved. The same goes for ad sets: a saved build that names an existing ad set must name the template ad's own ad set, or use a new ad set; to post into another existing ad set, copy from an ad in that ad set or launch directly without saving a build. A stored build that names another ad set can still launch from the CLI and MCP, but must be changed in the uploader before it launches there.

campaign.id and adSet.id must be real IDs of existing campaigns and ad sets. A blank ID is rejected rather than treated as a new destination; use campaign.name or adSet.name to create one.

When using copyFromAd, provide the upload batch, or launch a saved web build whose captured images have mediaHash and videos have mediaId. You can optionally set the campaign and ad set:

{
  "copyFromAd": "120233848667930472",
  "uploadId": "batch_abc123",
  "campaign": { "id": "120233848666410472" },
  "adSet": { "id": "120233848666620472" }
}

To find the right ad ID, drill through your account: ads campaigns then ads campaign <id> then ads adset <id> then ads ad <id>.

Profile Options

By default, new ads inherit the Facebook Page, Instagram account, and Threads profile from the template ad or preset. This is the same Profile Options control available via the Defaults panel in the web app. Override any of them with a profile block (or the --page / --instagram / --use-page-identity / --threads flags, which take precedence over the spec file):

{
  "copyFromAd": "120233848667930472",
  "uploadId": "batch_abc123",
  "profile": {
    "pageId": "123456789012345",
    "instagramId": "17841400000000000",
    "threadsId": "987654321098765"
  }
}

Run ads pages to list the Page IDs you can use, along with each Page's linked Instagram account.

If you override only the Page, Ads Uploader automatically uses its linked Instagram account. If no Instagram account is linked, it uses the Facebook Page as the Instagram identity. Threads still resets because it may belong to the old Page; set it explicitly when needed.

For an explicit Page-actor choice, use --use-page-identity or set "useFacebookPage": true inside profile. Do not combine it with --instagram or instagramId. You can also change just the Instagram or Threads profile without touching the Page by setting only those fields.

For multi-campaign launches, assign identities independently with profile.campaigns. Key each override by the matching campaign id (recommended) or by an unambiguous campaign name:

{
  "campaign": {
    "mode": "duplicate",
    "campaigns": [
      { "id": "prospecting", "name": "Prospecting" },
      { "id": "retargeting", "name": "Retargeting" }
    ]
  },
  "profile": {
    "campaigns": {
      "prospecting": { "pageId": "page_1", "instagramId": "ig_1", "threadsId": null },
      "retargeting": { "pageId": "page_2", "useFacebookPage": true, "threadsId": "threads_2" }
    }
  }
}

profile.campaigns only works when campaign.mode is "duplicate" or "split" with at least two entries in campaign.campaigns. Every key must match one of those campaigns; unmatched or ambiguous keys are rejected before launch.

Campaign Structure

By default, ads go into the template ad's campaign. You can create a new campaign by providing campaign.name.

For multi-campaign modes, use campaign.mode with a campaigns array:

{
  "campaign": {
    "mode": "duplicate",
    "campaigns": [
      { "name": "Campaign A" },
      { "name": "Campaign B" }
    ]
  }
}
ModeBehavior
"single"Default. One campaign.
"duplicate"All media is duplicated into each campaign.
"split"Media is split evenly across campaigns.

Ad Set Modes

By default, ads go into the template ad's existing ad set. The following modes give you control over how ads are distributed across ad sets.

Create a new ad set:

{ "adSet": { "name": "My Ad Set" } }

Use an existing ad set by ID:

{ "adSet": { "id": "120233848666620472" } }

One ad set per uploaded file:

{ "adSet": { "mode": "perUpload" } }

Auto-group into ad sets of a fixed size:

{ "adSet": { "mode": "autoGroup", "adsPerAdSet": 5 } }

Custom groups with full control over which files go where:

{
  "adSet": {
    "groups": [
      { "name": "Images - April 10", "media": ["hero.jpg", "banner.jpg"] },
      { "name": "Videos - April 10", "media": ["promo.mp4"] }
    ]
  }
}

Ad set naming pattern for multi-ad-set modes:

{ "adSet": { "mode": "perUpload", "namePattern": "Ad Set {index:01}" } }

Variant grouping groups ads by variation identifier into the same ad set:

{ "adSet": { "mode": "autoGroup", "groupVariations": true, "variationIdentifier": "-" } }

Budget and Bid-Control Override

Override the daily budget and/or bid amount on new ad sets. Values are in your account's currency units (e.g. 50 for $50 or 50 euros).

dailyBudget and bidAmount use account currency units. minimumRoas is a ratio, so 1.5 means a 1.5x ROAS goal.

  • ABO campaigns (budget lives on the ad set): combine dailyBudget with bidAmount for bid/cost-cap strategies, or with minimumRoas for LOWEST_COST_WITH_MIN_ROAS.
  • CBO campaigns (budget lives on the campaign): don't set dailyBudget on the ad set. Set campaign.dailyBudget or campaign.lifetimeBudget (mutually exclusive) to override the source campaign's budget, or omit both to inherit it. Ad-set minimum and maximum spend limits can use whole-unit amounts or 5-100% of the campaign budget in 5% steps; percentage limits also work with an existing CBO campaign. Set bidAmount or minimumRoas on the ad set when its source strategy uses that control.

bidAmount and minimumRoas are mutually exclusive because they belong to different bid strategies.

{ "adSet": { "dailyBudget": 50 } }
{ "adSet": { "bidAmount": 5 } }
{ "adSet": { "minimumRoas": 1.5 } }
{ "adSet": { "dailyBudget": 50, "bidAmount": 5 } }

For CBO spend limits in a spec, set these fields on adSet:

FieldWhat it sets
minSpendAd set minimum spend in whole currency units. 0 removes the limit copied from the source ad set.
maxSpendAd set maximum spend in whole currency units. 0 removes the limit copied from the source ad set.
minSpendPercentageAd set minimum spend as a percentage of the campaign budget (5 to 100, in steps of 5)
maxSpendPercentageAd set maximum spend as a percentage of the campaign budget (5 to 100, in steps of 5)
{
  "campaign": { "dailyBudget": 200 },
  "adSet": { "minSpend": 20, "maxSpendPercentage": 50 }
}

Also available as CLI flags: --daily-budget 50, --bid-amount 5, --minimum-roas 1.5, --campaign-daily-budget 100, --campaign-lifetime-budget 1000, --adset-min-spend 20, --adset-max-spend 80, --adset-min-spend-pct 20, and --adset-max-spend-pct 80.

Ad Set Targeting

Targeting applies to new ad sets. Omit the top-level targeting block to inherit the source ad set's audience unchanged. Any mode can use adSet.targetingPerAdSet, keyed like texts.perAdset by final ad set name, ad set ID, or collision-safe campaign::key. Custom mode also supports adSet.groups[].targeting. Precedence is targetingPerAdSet, then groups[].targeting, then the build default, then the source audience. Per-ad-set targeting is spec-only because CLI flags cannot address a specific planned ad set.

With multiple campaigns, each campaign's copy of an ad set is targeted separately; use "Campaign name::Ad set name" as the key for a specific copy.

{
  "targeting": {
    "countries": ["US"],
    "cities": [
      {
        "key": "2525495",
        "name": "Austin",
        "region": "Texas",
        "countryCode": "US",
        "radius": 25,
        "distance_unit": "mile"
      }
    ],
    "zips": [
      {
        "key": "US:90210",
        "name": "90210",
        "region": "California",
        "countryCode": "US"
      }
    ],
    "selectionVersion": 4,
    "ageSelected": true,
    "ageMin": 21,
    "ageMax": 55,
    "genderSelected": true,
    "genders": "women",
    "detailedTargetingSelected": true,
    "detailedTargetingGroups": [[
      {
        "id": "112002898811624",
        "name": "Advertising agency",
        "type": "work_employers"
      }
    ]]
  }
}
{
  "targeting": { "countries": ["US"], "ageMin": 21, "ageMax": 55 },
  "adSet": {
    "mode": "perUpload",
    "namePattern": "Ad Set {index:01}",
    "targetingPerAdSet": {
      "Ad Set 02": { "countries": ["CA"], "genders": "women" },
      "Ad Set 01": { "countries": ["GB"] }
    }
  }
}

For custom mode, groups[].targeting remains valid and keeps the override beside its media group:

{
  "adSet": {
    "mode": "custom",
    "groups": [{
      "name": "Canada Women",
      "media": ["canada.jpg"],
      "targeting": { "countries": ["CA"], "genders": "women" }
    }]
  }
}

Targeting rules:

  • Age and gender are opt-in. Set ageSelected: true or genderSelected: true, otherwise the age or gender values are ignored.
  • Detailed targeting replaces, it does not merge. detailedTargetingGroups becomes the complete detailed audience, so anything you leave out is dropped.
  • City radius is kept between 10 and 50 miles, or 17 and 80 kilometers. A value outside that range is moved to the nearest limit, and a missing radius uses the minimum.

Use ads targeting:search to find the city key (type city), the zip key such as US:90210 (type zip), and each detailed selection's id, name, and type (type detailed). Omit countries, cities, and zips to inherit the source locations. Countries, cities, and zips are combined as alternatives, so adding Austin or a zip to countries: ["US"] still targets the whole United States; omit countries to target only the city or zip. Zip codes have no radius.

Text Configuration

Use canonical arrays headlines, bodies, and descriptions. For uploaded media, string aliases headline, primaryText (or body), and description are accepted. Legacy aliases title / titles → headlines and linkDescription → descriptions accept strings or string arrays; websiteUrl → link accepts a string. These aliases work in common, per-ad and per-ad-set text blocks. Unknown text keys, invalid types, an alias with its canonical field, and multiple aliases for the same field are rejected.

Common text applies the same copy to all ads:

{
  "texts": {
    "common": {
      "headlines": ["Headline 1", "Headline 2"],
      "bodies": ["Primary text"],
      "descriptions": ["Description"]
    },
    "strategy": "flexible"
  }
}

Per-ad text lets you set unique copy for each file:

{
  "texts": {
    "perAd": {
      "hero.jpg": {
        "headlines": ["Hero Headline"],
        "bodies": ["Hero copy"],
        "descriptions": ["Hero desc"],
        "cta": "LEARN_MORE",
        "link": "https://example.com/hero",
        "urlTags": "utm_content=hero"
      },
      "banner.jpg": {
        "headlines": ["Banner Headline"],
        "bodies": ["Banner copy"]
      }
    }
  }
}

Per-ad keys are filenames (not full paths). If two uploads share a filename, a filename key is refused with AMBIGUOUS_MEDIA; rename one file, or key the text by its upload ID as media:<uploadId> (the error lists the IDs). Each entry supports: headlines, bodies, descriptions, cta, link, displayUrl, urlTags. Fields you don't specify inherit from the template ad.

Per-ad-set text applies one text block to every ad in a destination ad set. Use a plain ad set name/ID when it is unique, or a campaign::key entry when the same ad set name appears in more than one campaign:

{
  "texts": {
    "perAdset": {
      "Prospecting::prospecting_set": {
        "headlines": ["Prospecting headline"],
        "bodies": ["Prospecting copy"],
        "descriptions": ["Prospecting description"],
        "cta": "SHOP_NOW",
        "link": "https://example.com/prospecting",
        "displayUrl": "example.com/prospecting",
        "urlTags": "utm_campaign=prospecting",
        "aiDisclosure": false
      }
    }
  }
}

Every texts.perAdset key must match a planned ad set. Unmatched keys are rejected instead of falling back to common text.

Text presets let you load a saved text configuration:

{ "textPresetId": "preset_id_here" }

You cannot combine textPresetId with texts.

AI Disclosure

Self-disclose that an ad's creative was made or significantly edited with AI (Meta's AI-content self-disclosure). If you leave it out, the ad you copy settings from decides: its own disclosure is carried over, as in the Uploader. Set false to switch it off.

{ "aiDisclosure": true }

The top-level aiDisclosure field (or the --ai-disclosure flag) applies to every ad in the launch. You can also set aiDisclosure to true or false on an entry in texts.perAd or texts.perAdset; that per-entry value always wins, including an explicit false.

Strategy options control how multiple text variations are handled:

  • "flexible" (default) lets Meta optimize across your text variations. Multiple headlines and bodies become options that Facebook mixes and matches.
  • "separate" creates a separate ad for each text combination.

A top-level CTA applies to all ads. Per-ad CTAs in texts.perAd and per-ad-set CTAs in texts.perAdset override it.

{
  "cta": {
    "type": "SHOP_NOW",
    "link": "https://example.com",
    "displayUrl": "example.com"
  },
  "urlTags": "utm_source=facebook&utm_medium=paid"
}

Standard CTA types: LEARN_MORE, SHOP_NOW, SIGN_UP, SUBSCRIBE, GET_OFFER, GET_QUOTE, CONTACT_US, GET_IN_TOUCH, BOOK_TRAVEL (shown as Book Now), ORDER_NOW, BUY_NOW, APPLY_NOW, DOWNLOAD, SEE_DETAILS, WATCH_MORE, LISTEN_NOW, PLAY_GAME, DONATE_NOW, OPEN_LINK

Use the Meta value (such as SHOP_NOW), not the button label.

Objective-specific CTAs are inherited from the template ad and should not be set manually. Setting these on the wrong campaign type will cause a Facebook API error.

CTARequired Campaign Objective
MESSAGE_PAGEMessenger destination
WHATSAPP_MESSAGEWhatsApp destination
INSTAGRAM_MESSAGEInstagram DM destination
CALL_NOWCall campaign

URL Split-Test (Split Destination)

Provide 2 to 5 destination URLs under texts.urlVariants and every generated ad set is duplicated once per URL so Meta optimizes each ad and landing-page combination independently.

{
  "texts": {
    "common": { "headlines": ["Hero"], "bodies": ["Copy"] },
    "urlVariants": [
      { "link": "https://example.com/homepage", "label": "homepage" },
      { "link": "https://example.com/quiz", "label": "quiz-v2" }
    ]
  }
}
  • label is optional. When omitted, the last URL path slug is used (/quiz-v2 becomes quiz-v2), falling back to the hostname for root URLs.
  • In single-ad-set mode, each variant's label becomes the full ad set name.
  • In perUpload and autoGroup modes, each duplicated ad set's name gets _{label} added to the end (for example Ad Set 01_quiz-v2), or the label replaces a {destination} token if you include one. This matches the uploader.
  • texts.perAdset text applies to every copy of an ad set. Key it by the ad set's name without the label (for example Ad Set 01), not by a copy's name.
  • The {date} token in a label resolves to today's date (for example, launch-{date} becomes launch-2026-04-27).
  • Each destination URL must be unique. Identical URLs (or trailing-slash / case variants of the same URL) collapse to one entry, so make sure you have at least 2 distinct destinations.
  • Your ad set budget is multiplied by the number of variants, since each duplicate is its own ad set.
  • Not compatible with special-destination source ads (lead form, Messenger, WhatsApp, Instagram DM, Call). The CLI rejects this combination with a clear error, because those formats do not use cta.link.
  • Per-ad link overrides in texts.perAd lose to the variant URL when both are set.

Creative Enhancements

Control Advantage+ creative enhancements:

{ "creativeEnhancements": "none" }
ValueEffect
omittedInherit the enhancements of the template ad or preset
"metaDefaults"Deprecated. Sets no features, so every feature is sent as off.
"all"All features on
"none"All enhancements off; source formats inherited
["feature1", "feature2"]Only listed enhancements on, rest off; source formats inherited
{ "features": ["feature1", "feature2"] }Same as useMetaDefaults: false: only listed enhancements on, rest off; source formats inherited
{ "useMetaDefaults": false, "features": ["feature1"], "excludedFormats": ["format_carousel"] }Only listed enhancements on; explicitly turn off the source's carousel format
{ "feature1": true, "feature2": false }Set each listed feature on or off explicitly

Available features: text_translation, inline_comment, enhance_cta, text_optimizations, reveal_details_over_time, show_destination_blurbs, image_brightness_and_contrast, image_touchups, video_auto_crop, video_filtering, pac_relaxation, image_animation, image_templates, adapt_to_placement, product_extensions, product_tags, description_automation, add_text_overlay, music, carousel_to_video, carousel_dynamic_description, multi_share_end_card, multi_share_optimized

The show_destination_blurbs key is displayed as Show spotlights, and pac_relaxation as Flex media.

Format display options copy from the source ad by default, including with a feature list, "none", or { "useMetaDefaults": false }. To turn one off, use creativeEnhancements.excludedFormats with "format_carousel" (Show as carousel) and/or "format_collection" (Show as collection), or set the format to false in a per-feature object. Omit excludedFormats to keep both. Each excluded format must exist on the source and be eligible to copy; catalog-backed formats require an associated catalog. Unknown IDs, non-arrays, enabling and excluding the same ID, and excludedFormats on existing-post creatives are rejected. Formats cannot be enabled if the source lacks them.

When cherry-picking features, only list ones relevant to the media type. Video features (video_auto_crop, video_filtering) only apply to video ads. Carousel features (carousel_to_video, carousel_dynamic_description, multi_share_end_card, multi_share_optimized) only apply to carousel ads.

product_tags applies to image and video ads and is clone-only. It can be enabled only when the source ad has an associated catalog and explicit positioned product tags; all source tags and their positions are preserved. "all" includes it only for an eligible source and never invents a product tag.

Group uploaded files into carousel ads with per-card text and optional overall carousel text. cardTexts controls individual cards. Overall carousel text can be colocated on the carousel object or set in texts.perAd using the carousel name; colocated fields win when both are present.

{
  "carousel": [
    {
      "name": "My Carousel",
      "cards": ["slide1.jpg", "slide2.jpg", "slide3.jpg"],
      "headlines": ["Overall Carousel Headline"],
      "bodies": ["Overall primary text"],
      "descriptions": ["Overall description"],
      "cta": "SHOP_NOW",
      "link": "https://example.com/carousel",
      "urlTags": "utm_content=my_carousel",
      "cardTexts": [
        { "headline": "Slide 1", "description": "First card", "link": "https://example.com/1" },
        { "headline": "Slide 2", "description": "Second card", "link": "https://example.com/2" }
      ]
    }
  ]
}

Alternative overall-text shape:

{
  "texts": {
    "perAd": {
      "My Carousel": {
        "headlines": ["Overall Carousel Headline"],
        "bodies": ["Overall primary text"],
        "descriptions": ["Overall description"],
        "cta": "SHOP_NOW",
        "link": "https://example.com/carousel",
        "urlTags": "utm_content=my_carousel"
      }
    }
  },
  "carousel": [
    {
      "name": "My Carousel",
      "cards": ["slide1.jpg", "slide2.jpg", "slide3.jpg"]
    }
  ]
}

Cards must reference filenames from the upload batch or captured saved-build media. Each carousel needs 2 to 10 cards. Files claimed by a carousel are removed from the standard ad list.

Flexible Ads

Group multiple assets into a single flexible ad where Meta picks the best asset per placement:

{
  "flexible": [
    {
      "name": "Multi-Asset Ad",
      "assets": ["hero.jpg", "promo.mp4", "banner.jpg"]
    }
  ]
}

Each flexible group needs 2 to 10 assets. Files claimed by a flexible group are removed from the standard ad list.

Meta doesn't offer flexible ads in Leads campaigns. A flexible group copied from an ad in a Leads campaign is refused with FLEXIBLE_OBJECTIVE_UNSUPPORTED at preview, save and create, before anything is built. Use standard or Multi Media ads there, or copy from an ad in a Sales campaign.

Multi Media Ads

Group 2-10 uploaded images or videos into a Meta Multi Media ad:

{
  "multimedia": [
    {
      "name": "Mixed Media Ad",
      "assets": ["hero.jpg", "promo.mp4", "banner.jpg"],
      "assetTexts": [
        {
          "headline": "Hero headline",
          "primaryText": "Hero primary text",
          "description": "Hero description",
          "link": "https://example.com/hero",
          "displayUrl": "example.com/hero"
        }
      ]
    }
  ]
}

Assets must reference filenames from the upload batch or captured saved-build media. assetTexts is optional and lines up with assets by index; each field is a single override for that asset and blank fields fall back to the ad's main text and URLs. The ad's main text and URL apply to every asset, and an asset's override, the first asset's included, changes that asset only. Videos use a cached public thumbnail URL, recovered from Meta when missing. Files claimed by a Multi Media group are removed from the standard ad list.

Ad Naming

Customize how your ads are named:

{ "adNamePattern": "{filename} - {date}" }
PlaceholderWhat it inserts
{filename}Original filename without extension
{index}Position number (1, 2, 3...)
{index:01}Zero-padded position. The number sets the start and the padding: {index:01} gives 01, 02, 03; {index:50} gives 50, 51, 52.
{variation}Variation identifier if variant grouping is on
{campaign}Campaign name
{date}Current date (YYYY-MM-DD)
{date:short}Short date (MMDD)
{timestamp}Unix timestamp in milliseconds

Transforms wrap a value. An empty value means the filename:

TransformWhat it does
{split:_:2}Splits the filename on the delimiter (here _) and inserts the 2nd part
{clean:}Removes a trailing ratio suffix such as _9x16 or -1x1
{uppercase:}, {lowercase:}, {titlecase:}Change the case

Transforms can be nested, for example {titlecase:{split:_:2}}. A pattern can be up to 200 characters. See Ad Naming for more examples.

Options

{
  "options": {
    "status": "PAUSED",
    "pauseAt": "adSet",
    "schedule": {
      "startTime": "2026-04-01T09:00:00",
      "endTime": "2026-04-30T23:59:59"
    }
  }
}
FieldValuesDescription
status"PAUSED", "ACTIVE"Ad launch status (default: ACTIVE)
pauseAt"ad", "adSet", "campaign"Which level to pause at (default: ad)
schedule.startTimeISO 8601 stringAccount-local time YYYY-MM-DDTHH:mm[:ss], or ISO 8601 with an offset or Z (converted to the account time zone). Any minute; flag: --start-time <iso>.
schedule.endTimeISO 8601 stringAccount-local time YYYY-MM-DDTHH:mm[:ss], or ISO 8601 with an offset or Z (converted to the account time zone). Any minute; flag: --end-time <iso>. Lifetime budgets need an end at least 24 hours after the start.

Previews always return warnings and include lifetimeBudget when applicable. Create rejects new campaigns/ad sets with HTTP 400 for these codes (preview marks them blocking: true): LIFETIME_BUDGET_END_MISSING, LIFETIME_BUDGET_END_PASSED, LIFETIME_BUDGET_END_BEFORE_START, LIFETIME_BUDGET_END_TOO_SOON (set options.schedule.endTime or --end-time at least 24 hours after the start, or use a daily budget). EXISTING_AD_SET_ENDED and EXISTING_AD_SET_ENDS_SOON stay non-blocking warnings; extend the existing ad set in Ads Manager.

When the ads go into an existing ad set, options.schedule only works for Sales and App promotion campaigns, because Meta only supports per-ad schedules for those objectives. For other objectives, create a new ad set or remove options.schedule.

Spec Limits

LimitMaximum
Headlines, primary texts or descriptions in one ad (flexible text or a per-ad entry)5 of each
Headlines, primary texts or descriptions in a flexible ad, counting text copied from the template ad and the group's own text5 of each (Meta's limit; more is refused with FLEXIBLE_TEXT_LIMIT)
Text variants with the "separate" strategy50
Entries in texts.perAd or texts.perAdset200
Custom ad set groups (adSet.groups)50
Media in one custom group100
Carousel, flexible or Multi Media groups (each type)50
Cards or assets in one carousel, flexible or Multi Media group2 to 10
adNamePattern length200 characters

Upload and Variant Detection

Variant groups are detected automatically from filename conventions, just like in the web application. See Aspect Ratio Variations for full details on naming conventions.

Ratio suffixes: hero_1x1.jpg + hero_4x5.jpg + hero_9x16.jpg + hero_16x9.jpg + hero_1.91x1.jpg are grouped as one variant ad. Up to 5 ratios per group.

Token position: the ratio token can appear at the end (hero_4x5.jpg), in the middle (hero_4x5_v2.jpg), or at the start (4x5_hero.jpg).

Legacy word suffixes: hero.jpg + hero_vertical.jpg + hero_horizontal.jpg still work and map to 9x16 and 16x9.

The default delimiter is _. You can change it (or accept multiple) in Account > Defaults > Placements > Filename Separator.

Common Patterns

Upload and Create with a Preset

ads upload ./creatives/hero.jpg ./creatives/banner.jpg
ads create:preview spec.json
ads create spec.json

Where spec.json contains:

{ "adPresetId": "PRESET_ID", "campaign": { "name": "New Campaign" }, "uploadId": "BATCH_ID" }

Copy Settings from an Existing Ad

Browse your account to find the ad:

ads campaigns
ads campaign 120233848666410472
ads adset 120233848666620472
ads ad 120233848667930472

Then create a spec referencing it:

{
  "copyFromAd": "120233848667930472",
  "uploadId": "BATCH_ID"
}

The source ad can have its own creative settings or be built from an existing Page post, whose link, button and text are copied. Lead form, call and chat ads need no link. For other ads, if neither the ad nor its post has a website link, set cta.link. Otherwise preview and create are refused with a 400 before anything is created. The CLI refuses an ad with no creative settings to copy before it sends a create request.

Save an API Preset from an Existing Ad

ads presets:save --from-ad 120233848667930472 --name "Spring Purchase Template"
echo '{ "adPresetId": "PRESET_ID", "uploadId": "BATCH_ID", "campaign": { "id": "CAMPAIGN_ID" } }' > spec.json
ads create spec.json

This saves the same API preset shape used by the web app. The source ad must have inline creative settings; Page-post-backed ads cannot be saved as API presets.

Per-Ad Text with Unique Copy per File

{
  "adPresetId": "PRESET_ID",
  "campaign": { "name": "New Campaign" },
  "uploadId": "BATCH_ID",
  "texts": {
    "perAd": {
      "hero.jpg": {
        "headlines": ["Summer Sale Now On"],
        "bodies": ["Save up to 50% on all items"],
        "cta": "SHOP_NOW",
        "link": "https://example.com/summer"
      },
      "banner.jpg": {
        "headlines": ["New Collection Available"],
        "bodies": ["Browse our latest styles"],
        "cta": "LEARN_MORE",
        "link": "https://example.com/new"
      }
    }
  }
}

Auto-Group into Multiple Ad Sets

{
  "adPresetId": "PRESET_ID",
  "campaign": { "name": "New Campaign" },
  "uploadId": "BATCH_ID",
  "adSet": { "mode": "autoGroup", "adsPerAdSet": 3 }
}

Important Notes

  1. Always preview first. create:preview catches config errors before touching Facebook.
  2. Ads are active by default. Use --status PAUSED or "status": "PAUSED" in the spec to create them paused.
  3. uploadId comes from the upload output. It is the batch ID returned by ads upload.
  4. Uploads are tied to an ad account. Files are uploaded directly to the selected account's Facebook media library. The batch ID can only be used with the same account.
  5. copyFromAd needs resolvable media. Provide uploadId, or launch a saved build whose captured images have mediaHash and videos have mediaId. Optionally provide campaign.id and adSet.id to control placement.
  6. Per-ad text keys are filenames. Use "hero.jpg", not "/path/to/hero.jpg".
  7. textPresetId and texts are mutually exclusive. Use one or the other, not both.
  8. Objective-specific CTAs are inherited from the template. Do not set MESSAGE_PAGE, WHATSAPP_MESSAGE, etc. manually.

Partnership Ads with Your Own Media

The CLI and MCP can launch uploaded-media partnership ads on Facebook and Instagram. Use your normal image/video media, carousel, flexible, Multi Media, or placement-variant spec and add profile.partnership.enabled: true. Uploaded media follows ordinary assembly with a Second Identity. Omit uploaderMode or set it to false; true selects imported posts and cannot be used with uploads.

Choose your First Identity with profile.pageId and profile.instagramId. The shared partner is the Second Identity:

{
  "accountId": "act_123",
  "copyFromAd": "SOURCE_AD_ID",
  "adSet": { "id": "EXISTING_AD_SET_ID" },
  "mediaItems": [
    { "mediaName": "one.jpg", "mediaType": "image", "mediaHash": "UPLOADED_IMAGE_HASH_ONE" },
    { "mediaName": "two.jpg", "mediaType": "image", "mediaHash": "UPLOADED_IMAGE_HASH_TWO" }
  ],
  "profile": {
    "pageId": "111",
    "instagramId": "222",
    "partnership": {
      "enabled": true,
      "sponsorPageId": "333",
      "sponsorInstagramId": "444",
      "displayMode": "both"
    }
  },
  "options": { "status": "PAUSED" }
}

For different partners per ad, add this texts block to the same spec. The second row explicitly uses No Partner:

{
  "texts": {
    "mode": "perAd",
    "perAd": {
      "one.jpg": { "sponsorPageId": "555", "sponsorInstagramId": "666" },
      "two.jpg": { "sponsorPageId": null, "sponsorInstagramId": null }
    }
  }
}

For ad-set scopes, use texts.mode: "perAdset" and texts.perAdset, keyed by the final ad-set name/ID or campaign::key. In multi-campaign launches, use profile.campaigns[<id or unambiguous name>] for campaign sponsor overrides. Resolution is shared partner, then campaign, then the active row. Omitted sponsor fields inherit independently; explicitly set both IDs to null for No Partner. Switching only the partner Page does not clear an inherited Instagram ID on uploaded media; set that ID explicitly when changing the pair. Row First Identity fields (pageId, instagramId, threadsId) remain independent.

For an Instagram-only Second Identity, set sponsorPageId: null, sponsorInstagramId to the approved account ID, and sponsorPageUseInstagramAccount: true. A partner Facebook Page is also supported for uploaded media. The restriction on Facebook post imports does not apply to uploaded media.

displayMode applies across the launch: both (default), first, or dynamic. Each effective partner must differ from the First Identity. As in the Uploader, a partner whose approval is pending, or who has not approved partnership advertising for this First Identity yet, does not block the launch: preview and create return a PARTNERSHIP_APPROVAL_PENDING warning naming the partner, and Meta may create the ads in pending delivery until the partner approves. Every row needs a complete inherited or explicit partner, or an explicit No Partner override. Enabling partnerships without any sponsor is refused. Approval is rechecked at preview and create; a saved build does not store permission grants.

ads create:preview and ads_preview show each ad's effective partner, approval and header mode, grouped by ad set. Complete web-saved uploaded-media partnership builds can launch through the CLI and MCP; builds edited through these tools restore Partnership Ads, identities, row partners and header mode in the web uploader.

Instagram Existing-Post Partnership Spec

CLI JSON specs and the ads_preview / ads_create MCP tools accept Instagram posts with mediaItems[].kind: "partnershipPost". The source ad or preset supplies the settings; the imported post supplies its fixed creator identity and organic caption. Choose the shared sponsor in profile.partnership, or set sponsor and optional text overrides in texts.perAdset or texts.perAd. Headline, CTA, website URL and testimonial can be empty. A nonempty CTA needs a website URL. Facebook post imports are not supported.

Preview checks content and permissions through Meta read and validate-only calls, without creating ads or storing codes. Its code-free resolvedSpec can be saved, but codes must be supplied again at create. Creation checks permission again.

Use mediaItems[].kind: "partnershipPost" with platform: "instagram". Choose exactly one locator: sourceInstagramMediaId, import.postUrl, or import.instagramShortcode. A matching import.adCode may accompany one locator, or be used alone. The source ad or preset supplies settings, not the post. Optional creator.pageId and creator.instagramId assert the resolved creator.

{
  "accountId": "act_123",
  "copyFromAd": "456",
  "adSet": { "mode": "single", "id": "789" },
  "mediaItems": [{ "kind": "partnershipPost", "mediaName": "creator-post-one", "platform": "instagram", "import": { "postUrl": "https://www.instagram.com/p/POST_SHORTCODE/" } }],
  "profile": { "partnership": { "enabled": true, "uploaderMode": true, "sponsorPageId": "111", "sponsorInstagramId": "222", "displayMode": "both" } },
  "texts": { "mode": "common", "common": { "headline": "Discover the collection", "callToAction": "LEARN_MORE", "link": "https://example.com/", "multiAdvertiserAds": false } },
  "options": { "status": "PAUSED", "pauseAt": "ad" }
}

For a code-only import, use this complete request body with your IDs and a code inserted by an in-memory secret producer. Do not save the real code in a file:

{
  "accountId": "act_123",
  "copyFromAd": "456",
  "adSet": { "mode": "single", "id": "789" },
  "mediaItems": [{ "kind": "partnershipPost", "mediaName": "creator-post-one", "platform": "instagram", "import": { "adCode": "REPLACE_IN_MEMORY" } }],
  "profile": { "partnership": { "enabled": true, "uploaderMode": true, "sponsorPageId": "111", "sponsorInstagramId": "222", "displayMode": "both" } },
  "texts": { "mode": "common", "common": { "headline": "Discover the collection", "callToAction": "LEARN_MORE", "link": "https://example.com/", "multiAdvertiserAds": false } },
  "options": { "status": "PAUSED", "pauseAt": "ad" }
}

Use 1 to 250 posts per request, each with a unique mediaName of at most 200 characters. sponsorPageId must resolve to an actual brand Facebook Page for every planned ad; sponsorInstagramId is optional. Header displayMode is launch-wide: both, first, or dynamic. For imported posts, first means the creator-only header, labelled Partner identity only in the header in the uploader, not a brand-only header.

Per-campaign sponsors use profile.campaigns, keyed by campaign ID or an unambiguous campaign name, with sponsorPageId and optional sponsorInstagramId. Resolution is shared sponsor, then campaign, then the active per-ad-set or per-ad override. Put shared sponsor fields in profile.partnership, never texts.common; an adCode belongs in the row's import or an active per-ad/per-ad-set block. Creator pageId / instagramId values are assertions, not a way to change the post's author.

multiAdvertiserAds defaults to true; set it explicitly to false to opt out. CTA values must be Meta enums such as SHOP_NOW or LEARN_MORE, not labels such as Shop Now, and a nonempty CTA requires an http:// or https:// destination. The organic caption cannot be edited. Imported-post text blocks support one headline, CTA, link, URL tags, testimonial, AI disclosure and multi-advertiser choice. Each per-ad/per-ad-set override map supports at most 200 entries.

For imported posts, the CLI and MCP reject Facebook post imports, batches mixing imported posts and uploaded media, carousel/flexible/Multi Media structures, placement-variation grouping, and Threads identities. Uploaded media supports the normal creative formats and per-row partners. New Facebook post imports are currently unsupported in the web uploader too. There is no approved-post browser in the uploader; import an authorized Instagram URL, post ID or code.

For texts.mode: "perAd", key texts.perAd by mediaName. For texts.mode: "perAdset", key texts.perAdset by final ad set name/ID or campaign::key. Shared text uses texts.common. adSet.groups[].media contains those media names; reuse one media row across groups instead of importing the same source twice.

Use ads create:preview /dev/stdin --account act_123 for preview, then ads create /dev/stdin --account act_123 --status PAUSED to create. Pass sensitive codes only through stdin, never arguments, query strings or a file saved to disk. Supply codes again at creation; saved builds and preview output omit them. Use --account or set ads account act_123 first, even when the JSON contains accountId.

Admins can run ads create:test /dev/stdin --account act_123 --status PAUSED. It creates no Meta ads and reports metaValidation calls as passed, failed or not checked. A Meta refusal exits 1. Calls needing simulated IDs are not checked; passed validation does not guarantee delivery or appearance. Test Mode can save encrypted authorization codes and launch records.

Meta Edit Commands

CommandArguments And Options
edit:status<campaign|adset|ad> <ids...> --active|--paused
edit:archive, edit:delete<campaign|adset|ad> <ids...>
edit:rename<level> <id> <name>
edit:budget<campaign|adset> <id> --daily <n>|--lifetime <n>
edit:spend-cap<campaign-id> <n|none>
edit:spend-limits<adset-id> [--min <n>] [--max <n>]
edit:bid<campaign|adset> <id> [--strategy <S>] [--amount <n>] [--roas-floor <n>]
edit:schedule<campaign|adset> <id> [--start <ISO-time>] [--end <ISO-time|none>]
edit:targeting<adset-id> with targeting flags or --json-patch <file|->
edit:adset-settings<adset-id> with setting flags or --json-patch <file|->
edit:creative<ad-id> with creative flags or --json-patch <file|->
edit:apply<planId> and required confirmation flags
edit:undo<editId>; previews before applying
edits[--limit <n>] [--after <cursor>]
edit:show<editId>
edit:reviews[--limit <n>] [--after <cursor>]
edit:review<reviewId>
edit:cancel<reviewId> [--proposal <id>] [--duplicates-only]; edits cancel is the same command

All commands accept --account and --json. Edit and undo commands accept --preview-only. In direct mode, non-interactive writes require --yes plus applicable --confirm-spend, --confirm-live-change, --confirm-irreversible and --allow-large-change. Money inputs use major units. Edit preview/detail money values use minor units; History rows use readable currency amounts. See Meta Edits.

Edits are available to beta accounts while in beta. Enable them with Agent Edit on the Account Permissions page, as an account-wide default or per ad account. Approval is review mode: share the returned reviewUrl, then reuse --review-id <reviewId> on later previews. Review results have planId: null; apply in your Agent Inbox. EDITS_REVIEW_REQUIRED refuses agent apply while review mode is active. edit:reviews and edit:review show statuses and edit IDs as tables or --json.

Targeting And Ad Set Settings

ads edit:targeting 12345 --add-country CA --remove-interest 6000000000001 --age-min 25 --preview-only
ads edit:targeting 12345 --add-city '{"key":"123","radius":10,"distance_unit":"mile"}' --preview-only
ads edit:targeting 12345 --json-patch targeting-patch.json --preview-only
ads edit:adset-settings 12345 --optimization-goal VALUE --promoted-object '{"pixel_id":"9","custom_event_type":"PURCHASE"}' --preview-only
ads edit:bid adset 12345 --strategy LOWEST_COST_WITH_MIN_ROAS --roas-floor 2.5 --preview-only

Use discovered Meta IDs in place of examples. Targeting --add-* and --remove-* flags are repeatable: country, region, city, zip, geo-market, custom-location, excluded-country, excluded-region, excluded-city, excluded-zip, locale, interest, behavior, custom-audience, excluded-custom-audience, publisher-platform, facebook-position, instagram-position, audience-network-position, messenger-position and device-platform. Detailed-targeting search types also have singular flags, such as --add-work-employer and --remove-education-school. Use --help for the full list. Cities accept a key or JSON with radius and distance unit; custom locations require JSON coordinates, radius and unit.

Scalar flags are --age-min, --age-max, --genders all|men|women, --advantage-audience on|off, and --placements automatic. Several detailed-targeting OR groups require --group <zero-based-index>. Advantage+ retains a hard age floor of 18-25 and uses higher ages as suggestions. The table shows per-path differences followed by the full resulting audience.

--json-patch <file|-> takes the full patch object (add, remove, set, optional group), without op, level or id. Use - for stdin; do not combine JSON patches with patch flags. Unmentioned targeting is preserved. Undo restores the complete original audience.

Ad set setting flags are --optimization-goal, --billing-event, --promoted-object <json>, repeatable --attribution EVENT_TYPE:days, --incremental-attribution on|off, --dsa-beneficiary and --dsa-payor. This command also accepts --json-patch <file|-> for those API fields. Turn incremental attribution off before setting Standard attribution. Destination type is read-only.

Minimum ROAS requires VALUE optimization. --roas-floor 2.5 means 250% and belongs to the ad set. Under a campaign budget, use edit:bid campaign <id> --strategy LOWEST_COST_WITH_MIN_ROAS for the strategy, then set ad set floors. Targeting and settings retain the same review, preview, approval and undo behavior as the other edit commands.

Editing An Ad Creative

ads edit:creative <ad-id> changes an existing ad's creative through the usual preview and Edit Review flow. Use --primary-text, --headline, --description, --cta, --link <https-url>, --display-link and --url-tags (without ?). For multi-text creatives use repeatable --body, --title, --desc instead of single-text flags. Duplicate variants are refused. Empty display link or URL tags clears inheritance.

To change an image or video, create a new ad in the Uploader. Text-only edits keep the existing image or video.

--post <pageId_postId> switches to an existing Page post and accepts only --url-tags alongside it. --json-patch <file|-> accepts the API creative fields instead of these flags.

ads edit:creative 12345 --primary-text "Updated primary text" --account act_123 --preview-only
ads edit:creative 12345 --body "First variant" --body "Second variant" --preview-only

The table shows creative field values and warnings. Old-post likes, comments and shares do not carry over, except when switching to an existing post. Expect Meta review and a possible learning reset. Up to 10 creative edits fit in a plan. Undo reattaches the old creative. Existing-post sources refuse text edits. Plain and placement-customised carousels support primary text, whole-carousel link, CTA, display link and URL tags. Use --json-patch with cards: [{ card: 2, headline: "Updated headline", description: "Updated description" }] for per-card text; card numbers start at 1. Plain carousels also accept per-card link; placement-customised carousels refuse it. Scalar headline and description edits are refused on carousels. Flexible, catalog and partnership creatives point to the Uploader's copy-settings flow. Inspect error.details.orphanCreativeId if creation succeeded but attachment failed; never retry a partial result blindly.

Saved-build safety and issue reports

Save and update responses include warnings and display with totalAdSets, totalAds, and the ads in each ad set. The display uses the web uploader's grouping logic. Compare it with preview totals before launching. An imported-post configuration that the web editor cannot represent returns display.complete: false with a reason and null totals; resolve that limitation before comparing totals. Report mismatches, contradictory results, silently ignored fields, or broken tool descriptions using ads_report_issue or ads report:issue.

Group media and per-ad text keys resolve by upload ID, Meta media ID/hash, exact file name, then variant base name. Ambiguous references fail. Unmatched custom groups fail at preview and create. To reuse an existing destination, supply its campaign/ad-set ID explicitly.

Per-ad copy names ignored shared headlines, primary text and descriptions in warnings. Shared link, CTA, display URL and URL tags still apply unless a row overrides them. Group dailyBudget, lifetimeBudget and bidAmount use whole account currency units; minimumRoas is a ratio. Daily and lifetime budgets are exclusive; lifetime budgets require options.schedule.endTime. Ad-set budgets cannot be used under a campaign budget. These group budget controls are currently server/spec-only.

Existing library videos keep their Meta video ID. At preview/create, the server recovers a missing thumbnail from Meta and uploads only that image for its hash. If Meta has no usable thumbnail, the request fails with the video ID.

ads report:issue --tool ads_preview --expected "Two ad sets" --actual "One ad set" --build-id build_123 --severity error --json

POST /api/v1/issues accepts tool, expected, actual, and optional buildId, requestId, jobId, adIds, accountId, and severity (info, warning, error, critical). Text and IDs are capped and redacted. Reports are logged for investigation and return { "reported": true, "reference": "report_..." }. The limit is 10 reports per user per hour. Reporting is available to all CLI/MCP users.

Request missing capabilities

Use ads request:feature when a goal needs a tool, parameter, filter, action, or level that is not available. Agents should file the request themselves without asking the user, then explain the limitation and continue with the best workaround. Use ads report:issue when an existing tool misbehaves.

ads request:feature --goal "Pause seasonal ads" --missing "Bulk pause ads by label" --tools-tried ads_list_ads,ads_edit_preview --level ad --workaround "Pause ads individually" --json

--goal and --missing are required. --tools-tried, --account, --level, --workaround, and --json are optional. No account selection is required. Describe the missing capability without names, emails, ad copy, tokens, or other personal data.

Returns requested: true, a reference, status: 'recorded', duplicate, and timesRequested. Similar requests are grouped; the same user's repeat within 24 hours does not increase the count. Available to every CLI user, limited to 20 requests per user per hour. The equivalent MCP tool is ads_request_feature.

Post-ID duplication starts directly by default, with or without a beta grant. With the beta, the account’s Agent Launch setting decides, as for every agent copy: Approval sends it for approval and Off refuses it. Agent Edit does not apply.

Product Showcase

Product Showcase shows products from a catalog under a single image or video ad. Full catalog (dynamic/DPA) ads are not supported yet.

IDs must be strings of digits. Recommendations require Sales campaigns (OUTCOME_SALES); manual catalog/set choices also work on other objectives. Carousel, flexible, multi-media, existing-post and Partnership ads cannot enable Product Showcase.

Discover catalogs and sets, then request a recommendation:

ads catalogs list --account act_123
ads catalogs product-sets 3390205374500501 --product-set 29588799347389253 --json
ads catalogs recommend --url https://www.asphaltgold.com/collections/nike-sneaker --page 1234 --pixel 5678
ads create:preview spec.json --product-showcase recommended
ads create spec.json --product-showcase on --catalog 3390205374500501 --product-set 29588799347389253
ads create:preview spec.json --product-showcase off

Create and preview accept productShowcase: { enabled?, catalogId?, productSetId?, useRecommendation? }; enabled defaults to true. Omit the field to inherit the source creative. RECOMMENDED selects Meta’s default (choiceSource: "DEFAULT"); MUST_PICK requires a returned candidate ID as productSetId ("MANUAL"). MANUAL requires a catalog and product set. A validated override of Meta’s default uses "MANUAL".

CLI create and preview flags: --product-showcase <on|off|recommended>, --catalog <id>, --product-set <id>. IDs alone mean on; recommended maps to useRecommendation: true. Off maps to { enabled: false } and cannot include IDs; it disables showing products while keeping the source ad’s own set attached.

Preview returns productShowcase with enabled, catalog/set IDs, optional set name, choiceSource and source (recommendation, manual, source_ad), or null when omitted. Recommendations are read-only and do not enable the feature. Test Mode makes no catalog calls, requires explicit catalog/set IDs when enabled and notes that membership/recommendations were not verified. Saved UI choices map to Product Showcase; saved defaults are checked again before launch.