Meta Marketing CLI is a developer-first, fail-closed command-line interface for running Meta marketing operations end-to-end without Ads Manager. It combines automated auth setup, direct Graph API access, campaign/ad lifecycle workflows, Instagram publishing, reliability checks, and enterprise controls behind a stable machine-readable output contract.
- Automated auth setup with browser callback (
meta auth setup) - Keychain-only secret storage (no env/plaintext fallback)
- Fail-closed profile preflight before operational commands
- Direct Graph API access (
api get/post/delete/batch) - Insights account discovery, raw action-type discovery, publisher-platform filtering, and metric packs (
insights accounts list,insights action-types,insights run --metric-pack quality|local_intent) - Campaign, ad set, ad, creative (image + video), audience, and catalog workflows
- Budget mutation guardrails for spend-changing writes
- Instagram media upload, status, publish, schedule lifecycle, and raw account/media insights
- Operations intelligence checks (schema drift, rate limits, policy preflight)
- Stable output envelope (
contract_version: 1.0) for automation
- Go
1.23+ - macOS Keychain or Linux Secret Service (required)
jq(recommended for scripting and smoke checks)
go install github.com/bilalbayram/metacli/cmd/meta@latest
meta --helpgo build -o meta ./cmd/meta
./meta --helpCreate a Meta app in the developer dashboard and collect your app credentials.
Use this callback URI pattern for CLI auth automation:
REDIRECT_URI=https://<REDIRECT_URI>/oauth/callbackYou must allow this redirect URI in your app OAuth settings, Meta doesn't allow http, you can use cloudflared or similar service for it:
cloudflared tunnel --url http://127.0.0.1:53682Scope packs are used by auth setup to request a practical scope set for your workflow (solo_smb, ads_only, ig_publish).
Official docs:
- Facebook Login
- Permissions Reference
- Access Tokens
- Long-Lived Access Tokens
- Instagram Content Publishing
- Marketing APIs
./meta auth setup \
--profile prod \
--app-id <APP_ID> \
--app-secret <APP_SECRET> \
--mode both \
--scope-pack solo_smb \
--listen 127.0.0.1:53682 \
--timeout 180s \
--open-browser
./meta auth validate \
--profile prod \
--min-ttl 72h./meta --profile prod api get act_<AD_ACCOUNT_ID>/campaigns \
--fields id,name,status \
--follow-next \
--limit 100
./meta --profile prod api post act_<AD_ACCOUNT_ID>/campaigns \
--params "name=Launch Campaign,objective=OUTCOME_SALES,status=PAUSED"
./meta --profile prod api delete <OBJECT_ID>Schema-aware commands look in ~/.meta/schema-packs by default. Run ./meta schema sync first if you have not populated that directory yet.
./meta --profile prod campaign create \
--account-id <AD_ACCOUNT_ID> \
--params "name=Launch Campaign,objective=OUTCOME_SALES,status=PAUSED"
./meta --profile prod adset create \
--account-id <AD_ACCOUNT_ID> \
--params "name=Prospecting,campaign_id=<CAMPAIGN_ID>,status=PAUSED,billing_event=IMPRESSIONS,optimization_goal=OFFSITE_CONVERSIONS"
./meta --profile prod ad create \
--account-id <AD_ACCOUNT_ID> \
--params "name=Launch Ad,adset_id=<ADSET_ID>,status=PAUSED" \
--json '{"creative":{"creative_id":"<CREATIVE_ID>"}}'# Discover active ad accounts first
./meta --profile prod insights accounts list \
--active-only \
--output table
# Run campaign insights with expanded quality metrics
./meta --profile prod insights run \
--account-id <AD_ACCOUNT_ID> \
--date-preset last_7d \
--level campaign \
--metric-pack quality \
--format jsonl
# Discover raw action types returned by actions and cost_per_action_type
./meta --profile prod insights action-types \
--account-id <AD_ACCOUNT_ID> \
--date-preset last_30d \
--level ad \
--format json
# Keep paid insights on Instagram placements only
./meta --profile prod insights run \
--account-id <AD_ACCOUNT_ID> \
--date-preset last_7d \
--level account \
--metric-pack local_intent \
--publisher-platform instagram \
--format json
# Run local-intent insights and keep raw action arrays plus flat aliases
./meta --profile prod insights run \
--account-id <AD_ACCOUNT_ID> \
--date-preset last_30d \
--level campaign \
--metric-pack local_intent \
--format jsonNotes:
insights runstill fails closed when--account-idis missing.--metric-pack basickeeps the previous default behavior.--metric-pack qualityrequests expanded fields (CTR, CPC, CPM, reach/frequency, actions, and related cost metrics).--metric-pack local_intentpreserves rawactionsandcost_per_action_type, then adds flat alias fields likeaddress_taps,calls,directions, andprofile_visitswhen those raw action types are present.--publisher-platform instagramkeeps the paid Ads Insights surface but forces apublisher_platformbreakdown and filters the returned rows down to Instagram placements.callsis populated from the raw Graph action typeclick_to_call_native_call_placed; other call-connect and call-confirm metrics remain raw-only so machine consumers can keep those semantics separate.- Place-navigation aliases are intentionally sparse.
address_taps,directions, andprofile_visitsonly appear when Graph emits the matching raw action types for your campaigns. insights action-typesis the quickest way to discover which rawaction_typevalues your account is returning before you automate against them.
./meta --profile prod ig caption validate \
--caption "Launch post #meta #ads" \
--strict
./meta --profile prod ig media upload \
--ig-user-id <IG_USER_ID> \
--media-url https://cdn.example.com/image.jpg \
--caption "Launch post #meta"
./meta --profile prod ig publish feed \
--media-url https://cdn.example.com/image.jpg \
--caption "Launch post #meta" \
--idempotency-key publish-feed-001
# Schedule a story for next Tuesday at 4 PM UTC
./meta --profile prod ig publish story \
--media-url https://cdn.example.com/story.mp4 \
--caption "Coming soon" \
--media-type STORIES \
--publish-at 2026-03-17T16:00:00Z
# Execute all due scheduled publishes (designed for cron)
./meta --profile prod ig publish schedule run
# Preview what would be published without executing
./meta --profile prod ig publish schedule run --dry-run
# List scheduled jobs
./meta --profile prod ig publish schedule list --status scheduled# Fetch raw Instagram account insights
./meta --profile prod ig insights account run \
--metric profile_views \
--period day \
--metric-type total_value \
--since 2026-03-14 \
--until 2026-03-21 \
--format json
# Discover recent media ids plus identity fields for follow-up insights calls
./meta --profile prod ig insights media list \
--limit 10 \
--format json
# Fetch raw organic media insights for one or more posts
./meta --profile prod ig insights media run \
--media-id <MEDIA_ID> \
--metric profile_visits \
--period lifetime \
--format json
# Fetch Instagram account local-intent summary plus raw metric payloads
./meta --profile prod ig insights account local-intent \
--since 2026-03-14 \
--until 2026-03-21 \
--format json
# Combine paid Instagram Ads Insights with Instagram account local-intent metrics
./meta --profile prod ig insights local-intent \
--account-id <AD_ACCOUNT_ID> \
--date-preset last_7d \
--format jsonNotes:
ig insights account runstays close to the raw Instagram account-insights metric objects returned by Graph.ig insights media runuses the Instagram media insights surface, which Meta documents as organic-only.ig insights account local-intentpreserves raw metric objects and adds normalized summary fields likecalls,directions,email_contacts,text_contacts,book_now, andprofile_viewswhen the corresponding contact button breakdowns are present.ig insights local-intentreturns a layered object withpaid_instagram,instagram_account, and a mergedsummaryso the paid and Instagram-account surfaces stay distinguishable.
Cron setup (run every 30 minutes):
*/30 * * * * /usr/local/bin/meta --profile prod ig publish schedule run
Primary command families:
campaign:list,create,update,pause,resume,cloneadset:list,create,update,pause,resumead:list,create,update,pause,resume,clonecreative:upload,upload-video,createaudience:create,update,delete,list,getcatalog:upload-items,batch-items
Creative video upload example:
./meta --profile prod creative upload-video \
--account-id <AD_ACCOUNT_ID> \
--file ./assets/launch.mp4 \
--wait-ready \
--timeout 10m \
--poll-interval 5sCampaign/ad set/ad list examples:
# List campaigns with shared read filters
./meta --profile prod campaign list \
--account-id <AD_ACCOUNT_ID> \
--fields id,name,status,effective_status,objective \
--name launch \
--status ACTIVE,PAUSED \
--effective-status ACTIVE \
--active-only \
--page-size 50 \
--follow-next \
--limit 100
# List ad sets scoped to one campaign
./meta --profile prod adset list \
--account-id <AD_ACCOUNT_ID> \
--campaign-id <CAMPAIGN_ID> \
--fields id,name,status,effective_status,campaign_id
# List ads with campaign+adset intersection filtering
./meta --profile prod ad list \
--account-id <AD_ACCOUNT_ID> \
--campaign-id <CAMPAIGN_ID> \
--adset-id <ADSET_ID> \
--fields id,name,status,effective_status,campaign_id,adset_idBudget mutation guardrail example:
./meta --profile prod campaign update \
--campaign-id <CAMPAIGN_ID> \
--params "daily_budget=5000" \
--confirm-budget-changeAudience read examples:
# List audiences (default kind=all includes custom + saved)
./meta --profile prod audience list \
--account-id <AD_ACCOUNT_ID>
# List only custom audiences
./meta --profile prod audience list \
--account-id <AD_ACCOUNT_ID> \
--kind custom
# List only saved audiences with selected fields and pagination controls
./meta --profile prod audience list \
--account-id <AD_ACCOUNT_ID> \
--kind saved \
--fields id,name,subtype,time_updated \
--limit 100 \
--follow-next
# Get one audience by ID
./meta --profile prod audience get \
--audience-id <AUDIENCE_ID> \
--fields id,name,subtype,time_updated,retention_daysig media upload|statusig publish feed|reel|storyig publish schedule list|cancel|retry- Plugin namespace stubs:
wa,msgr,threads,capi
./meta --output json ops init --state-path "$HOME/.meta/ops/baseline-state.json"
./meta --output json ops run \
--state-path "$HOME/.meta/ops/baseline-state.json" \
--preflight-config-path "$HOME/.meta/config.yaml"./meta enterprise mode cutover \
--legacy-config ~/.meta/config.yaml \
--config ~/.meta/enterprise.yaml \
--org agency \
--org-id org_1 \
--workspace prod \
--workspace-id ws_1 \
--principal ops.admin
./meta enterprise execute \
--config ~/.meta/enterprise.yaml \
--principal ops.admin \
--command "auth rotate" \
--workspace agency/prod \
--approval-token <GRANT_TOKEN> \
--correlation-id corr-20260224-001 \
--require-secret auth_rotation_key:rotate# 1) Automated auth
./meta auth setup --profile prod --app-id <APP_ID> --app-secret <APP_SECRET> --mode both --scope-pack solo_smb
# 1.5) Sync schema packs into ~/.meta/schema-packs
./meta schema sync
# 2) Create campaign + ad set + creative + ad
./meta --profile prod campaign create --account-id <AD_ACCOUNT_ID> --params "name=CLI Campaign,objective=OUTCOME_SALES,status=PAUSED"
./meta --profile prod adset create --account-id <AD_ACCOUNT_ID> --params "name=CLI AdSet,campaign_id=<CAMPAIGN_ID>,status=PAUSED,billing_event=IMPRESSIONS,optimization_goal=OFFSITE_CONVERSIONS"
./meta --profile prod creative create --account-id <AD_ACCOUNT_ID> --params "name=CLI Creative,object_story_id=123_456"
./meta --profile prod ad create --account-id <AD_ACCOUNT_ID> --params "name=CLI Ad,adset_id=<ADSET_ID>,status=PAUSED" --json '{"creative":{"creative_id":"<CREATIVE_ID>"}}'
# 3) Publish IG content
./meta --profile prod ig publish feed --media-url https://cdn.example.com/launch.jpg --caption "Shipped from CLI" --idempotency-key launch-feed-001
# 4) Run ops check
./meta --profile prod --output json ops run --state-path "$HOME/.meta/ops/baseline-state.json"token_type |
Primary Use | Required Profile Keys (v2) | Lifecycle Path |
|---|---|---|---|
system_user |
Business automation and system flows | app_id, business_id, token_ref, app_secret_ref, auth metadata fields |
Added via auth add system-user; validated by preflight; hard-fails on invalid/TTL/scope issues |
user |
OAuth user context for marketing + IG | app_id, token_ref, app_secret_ref, auth metadata fields |
Created via auth setup/auth login; long-lived exchange + debug validation enforced |
page |
Page-scoped actions | app_id, page_id, source_profile, token_ref, app_secret_ref, auth metadata fields |
Derived via auth page-token; source credentials and preflight checks required |
app |
App-level service token operations | app_id, token_ref, app_secret_ref, auth metadata fields |
Created via auth app-token set; rotatable via auth rotate |
Auth metadata fields required on every profile in schema v2:
auth_providerauth_modescopesissued_atexpires_atlast_validated_at
| Command Family | Purpose | Key Commands |
|---|---|---|
auth |
Authentication and profile/token lifecycle | add system-user, setup, login, discover, page-token, app-token set, validate, rotate, debug-token, list |
api |
Direct Graph API access | get, post, delete, batch |
insights |
Reporting queries and export | accounts list, run |
lint |
Request lint against schema packs | request |
schema |
Local schema pack management | list, sync |
changelog |
Version/change checks | check |
| Command Family | Purpose | Key Commands |
|---|---|---|
campaign |
Campaign lifecycle | list, create, update, pause, resume, clone |
adset |
Ad set lifecycle | list, create, update, pause, resume |
ad |
Ad lifecycle | list, create, update, pause, resume, clone |
creative |
Creative assets | upload, upload-video, create |
audience |
Audience lifecycle | create, update, delete, list, get |
catalog |
Catalog item ingestion/mutation | upload-items, batch-items |
| Command Family | Purpose | Key Commands |
|---|---|---|
ig |
Instagram publishing lifecycle | health, media upload, media status, caption validate, publish feed, publish reel, publish story, publish schedule list/cancel/retry/run |
wa |
WhatsApp namespace scaffold | health, capability |
msgr |
Messenger namespace scaffold | health, capability |
threads |
Threads namespace scaffold | health, capability |
capi |
Conversions API namespace scaffold | health, capability |
| Command Family | Purpose | Key Commands |
|---|---|---|
ops |
Reliability checks and report pipeline | init, run |
enterprise |
Org/workspace authorization and execution governance | context, authz check, execute, mode cutover, approval request, approval approve, approval validate, policy eval |
Global flags (all commands):
--profile <name>--output json|jsonl|table|csv--debug
Use ./meta <family> --help and ./meta <family> <command> --help for full flag-level details.
Most commands emit the stable envelope with contract_version: "1.0":
contract_versioncommandtimestamprequest_idsuccessdatapagingrate_limiterror
Error payload contract (when success=false):
type,code,error_subcode,status_code,message,fbtrace_id,retryableremediation:category,summary,actions[],fields[]diagnostics: raw Meta error diagnostics for classifier coverage gaps
1: unknown failure2: config failure3: auth failure4: input/validation failure5: API failure
- Secrets are stored in OS keychain only (
meta-marketing-cliservice namespace) - Config references secrets by keychain ref (
keychain://...) - Commands fail closed when required auth/config/schema data is missing or invalid
- No hidden fallback to environment variables or plaintext secrets