Documentation

Ad Configuration

CLI Full Reference

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

Commands

Authentication

CommandWhat it does
ads loginAuthenticate via browser (opens your default browser)
ads logoutClear stored credentials
ads whoamiShow the currently logged-in user
ads configShow configuration (account, API URL, credentials path)

Browsing

CommandWhat it does
ads accountsList all ad accounts connected to your Meta 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 "advertising" --type detailedFind detailed-targeting IDs and types
ads campaignsList active campaigns
ads campaigns --status allInclude paused and archived campaigns
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 adset <id>Show the ads inside an ad set
ads ad <id>View full ad details including creative settings
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
ads text-presetsList your saved text presets
ads text-presets <id>Show details for a specific text preset
ads uploadsList recent upload batches
ads uploads <batchId>Show batch details (files, variants, hashes)

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

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)

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)

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

Create Flags

These flags apply to ads create, ads create:preview, and ads create:interactive. They can be used instead of or alongside a spec file.

FlagDescription
--account <id>Override default ad account
--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)
--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)
--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 by default.
--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

Browse Flags

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

FlagDescription
--status <status>active (default) or all
--inactiveShorthand for --status all (on campaigns)
--search <text>Filter by name (on campaigns, adsets)

City targeting 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 "advertising" --type detailed
FlagDescription
--type <type>Required. city returns targeting.cities[].key values; detailed returns entries for targeting.detailedTargetingGroups.
--account <id>Override the configured default ad account
--limit <n>Return 1-25 matches
--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.

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)

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 also need thumbnailHash; Multi Media videos use their captured public thumbnail URL.

Minimal Spec

{
  "adPresetId": "your_preset_id",
  "uploadId": "batch_abc123"
}

Full Example

{
  "adPresetId": "preset_id_here",
  "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. Locks in the campaign, ad set, and ad config.
copyFromAdA Facebook ad ID to copy settings from.

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" }
    }
  }
}

Every profile.campaigns key must match a campaign in campaign.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 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 are independent Meta fields:

  • ABO campaigns (budget lives on the ad set): you can set dailyBudget, bidAmount, or both. Bid-cap strategies like COST_CAP, LOWEST_COST_WITH_BID_CAP, and TARGET_COST require a bidAmount alongside the budget.
  • 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. For bid-cap strategies, set only bidAmount on the ad set.
{ "adSet": { "dailyBudget": 50 } }
{ "adSet": { "bidAmount": 5 } }
{ "adSet": { "dailyBudget": 50, "bidAmount": 5 } }

Also available as CLI flags: --daily-budget 50, --bid-amount 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 targeting block to inherit the source ad set's audience unchanged.

{
  "targeting": {
    "countries": ["US"],
    "cities": [
      {
        "key": "2525495",
        "name": "Austin",
        "region": "Texas",
        "countryCode": "US",
        "radius": 25,
        "distance_unit": "mile"
      }
    ],
    "selectionVersion": 4,
    "ageSelected": true,
    "ageMin": 21,
    "ageMax": 55,
    "genderSelected": true,
    "genders": "women",
    "detailedTargetingSelected": true,
    "detailedTargetingGroups": [[
      {
        "id": "112002898811624",
        "name": "Advertising agency",
        "type": "work_employers"
      }
    ]]
  }
}

Use ads targeting:search to find the city key and each detailed selection's id, name, and type. Omit both countries and cities to inherit the source locations. Countries and cities are combined as alternatives, so adding Austin to countries: ["US"] still targets the whole United States; omit countries to target only the city.

Text Configuration

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

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, CONTACT_US, DOWNLOAD, ORDER_NOW, BUY_NOW, BOOK_NOW, APPLY_NOW, GET_QUOTE, GET_IN_TOUCH, WATCH_MORE, PLAY_GAME

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–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-v2quiz-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 appends _{label} to the pattern (or substitutes a {destination} token if you include one).
  • The {date} token in a label resolves to today's date (e.g., launch-{date}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 - those formats don't route via 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
omittedAll features off
"metaDefaults"Deprecated alias for "none"
"all"All features on
"none"All features off
["feature1", "feature2"]Only listed features on, rest off

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.

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. Minimum 2 cards per carousel. 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"]
    }
  ]
}

Minimum 2 assets per group. Files claimed by a flexible group are removed from the standard ad list.

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 primary rendered asset uses the ad's main text and URL, and any primary asset text override becomes the first main text option. Videos need a captured cached public thumbnail URL. 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:01}Zero-padded index (01, 02, 03...)
{variation}Variation identifier if variant grouping is on
{campaign}Campaign name
{date}Current date (YYYY-MM-DD)
{date:short}Short date (MM-DD)
{timestamp}Unix timestamp

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 stringScheduled start time (uses ad account timezone)
schedule.endTimeISO 8601 stringScheduled end time (optional)

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", "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 must have inline creative settings. If it was built from an existing Page post, the CLI will reject it before submitting a create request.

Save an API preset from an existing ad

ads presets:save --from-ad 120233848667930472 --name "Spring Purchase Template"
ads create --preset PRESET_ID --upload BATCH_ID

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",
  "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",
  "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.