Kanzen Ads Docs
IntroductionQuickstartAuthentication
Connect to Hermes Agent
Payments / Top-up
Overview & CreditsGenerate ImageGenerate VideoRetrieve Media
Top Up Campaign AdsCharacter + Product Video
Errors & LimitsAPI ReferenceMeta Marketing API Guide

Meta Marketing API — Reference Guide

A working reference for the Meta Marketing API surface Kanzen Ads and connected agents operate against — object hierarchy, regular ad operations, Collaborative Ads (CPAS), auth/rate-limit mechanics, and a ready-to-use cookbook. Pinned to API version v25.0.

❗

Verify-don't-trust

Every field/endpoint below comes from Meta's official reference. Points not yet 100% confirmed are flagged ⚠️ VERIFY — test in Graph API Explorer with a real token before relying on them in production. Never hardcode a minimum/rule that's flagged as a heuristic.

0. Basics

Object hierarchy:

text
Business Manager (business_id)
 └─ Ad Account  act_{ad_account_id}
     └─ Campaign      → objective, budget (if CBO)
         └─ Ad Set    → targeting, budget (if ABO), schedule, bid, optimization
             └─ Ad    → links Ad Set + Creative
                 └─ Ad Creative  → object_story_spec / object_story_id
 ├─ Pages (page_id) · Instagram (ig_user_id) · Pixels/Datasets · Product Catalogs

An ad account is always referenced with the act_ prefix (e.g. act_123456789).

Auth (summary — details in §C1): tokens are sent as a query/body param access_token=<TOKEN> (NOT a Bearer header — that's what the official doc guarantees). For server-to-server production use a System User token (no expiry). Core scopes: ads_management (write), ads_read (read), business_management, catalog_management.

Base call: GET/POST/DELETE https://graph.facebook.com/v25.0/{node-or-edge}?...&access_token=<TOKEN>

Part A — Regular Ops (non-CPAS)

Standard campaign/ad set/ad operations used for the majority of ads (non-collaborative). For CPAS, see Part B.

A1. Campaign

Doc: /reference/ad-campaign-group/ (internal node name is ad-campaign-group).

Key fields: name, objective, status (ACTIVE/PAUSED/DELETED/ARCHIVED), effective_status, special_ad_categories (required — [] / NONE unless it's a special category), bid_strategy, daily_budget/lifetime_budget (CBO only — see A3), spend_cap, promoted_object.

Objective (ODAX — current): OUTCOME_SALES, OUTCOME_LEADS, OUTCOME_ENGAGEMENT, OUTCOME_TRAFFIC, OUTCOME_AWARENESS, OUTCOME_APP_PROMOTION. (Legacy CONVERSIONS/LINK_CLICKS/etc. still return on old campaigns, but can't be used to create new ones.)

Campaign — create / read
# Create
curl -X POST -F 'name=Campaign X' -F 'objective=OUTCOME_TRAFFIC' \
  -F 'status=PAUSED' -F 'special_ad_categories=[]' -F 'access_token=<TOKEN>' \
  https://graph.facebook.com/v25.0/act_<AD_ACCOUNT_ID>/campaigns
# → {"id":"<CAMPAIGN_ID>"}

# Read
GET /v25.0/<CAMPAIGN_ID>?fields=id,name,objective,status,effective_status,daily_budget,lifetime_budget,bid_strategy

A2. Ad Set

Doc: /reference/ad-campaign/ (internal node name is ad-campaign, but this is the Ad Set document).

Key fields: campaign_id (required), daily_budget/lifetime_budget (ABO only), billing_event, optimization_goal, bid_amount/bid_strategy, targeting, promoted_object, start_time/end_time, attribution_spec, status. optimization_goal × billing_event must be a valid combination for the campaign's objective (enforced server-side — ⚠️ VERIFY per objective).

Ad Set — create
curl -X POST -F 'name=Adset A' -F 'campaign_id=<CAMPAIGN_ID>' \
  -F 'daily_budget=100000' -F 'billing_event=IMPRESSIONS' -F 'optimization_goal=OFFSITE_CONVERSIONS' \
  -F 'targeting={"geo_locations":{"countries":["ID"]},"age_min":18,"age_max":65}' \
  -F 'status=PAUSED' -F 'access_token=<TOKEN>' \
  https://graph.facebook.com/v25.0/act_<AD_ACCOUNT_ID>/adsets

A3. Budget — CBO vs ABO, scale up/down, pause/resume

ModeBudget lives onBehavior
CBO (Advantage campaign budget)Campaign (daily_budget/lifetime_budget)Meta distributes spend across ad sets automatically. Ad sets must NOT have their own budget.
ABO (ad set budget)Ad Set (daily_budget/lifetime_budget)Budget is fixed per ad set; Meta does not reallocate across ad sets.
⚠️

Budget is in minor currency units

daily_budget=100000 on an IDR account is not a safe assumption to hardcode — some currencies' minor-unit behavior differs (e.g. IDR/JPY). Always check GET /act_{id}?fields=currency first. USD example: 2000 = $20.00.
  • Scaling up/down: ALWAYS read the current value first, compute, then POST — never blind-write.
  • CBO → POST to {campaign_id}. ABO → POST to {adset_id}.
  • ">20% change resets the learning phase" is a practitioner heuristic, not an official API contract — don't hardcode it as a Meta rule. What's confirmed to reset learning: changing targeting/optimization_goal/bid_strategy.
  • Daily budget can over-spend up to ~25% on high-opportunity days, balanced out weekly.
Budget scale / pause / resume
# SCALE UP +30% (CBO campaign)
GET /v25.0/<CAMPAIGN_ID>?fields=daily_budget            # → "10000"  ($100)
POST /v25.0/<CAMPAIGN_ID>  -F 'daily_budget=13000'      # $130
GET /v25.0/<CAMPAIGN_ID>?fields=daily_budget            # readback

# SCALE DOWN -30% (ABO adset)
GET /v25.0/<ADSET_ID>?fields=daily_budget               # → "5000"
POST /v25.0/<ADSET_ID>  -F 'daily_budget=3500'

# PAUSE / RESUME (campaign or adset)
POST /v25.0/<ADSET_ID>  -F 'status=PAUSED'
POST /v25.0/<ADSET_ID>  -F 'status=ACTIVE'
# ⚠️ If the parent campaign is PAUSED, resuming the ad set does not deliver —
# effective_status stays CAMPAIGN_PAUSED until the campaign is resumed too.

A4. Ad & Ad Creative

Doc: /reference/adgroup/ (Ad), /reference/ad-creative/.

Ad: adset_id (required, immutable after create), creative ({"creative_id":"<ID>"} or inline), status (create only accepts ACTIVE/PAUSED, then auto-transitions to PENDING_REVIEW).

Creative — two ways:

  • (a) object_story_spec — creates a new post (usually dark/unpublished) at creative-creation time: page_id + link_data/video_data/photo_data + call_to_action + image_hash (pre-uploaded image) or picture (URL). For IG placement use instagram_user_id (NOT the old instagram_actor_id — deprecated Sept 2025).
  • (b) object_story_id — uses an already-published post: format {page_id}_{post_id}. If a creative for that story id already exists, the API returns the existing creative_id (idempotent).
⚠️

image_hash vs. attachment_hash

The v25.0 reference consistently uses image_hash (top-level on AdCreative for image ads, and inside link_data for link/carousel ads). The literal field attachment_hash does not appear in the v25.0 reference — if older Kanzen Ads code uses it, treat as ⚠️ VERIFY (possibly an old API version or an internal naming). Advantage+/Dynamic creative uses asset_feed_spec (arrays of images/bodies/titles/CTAs that Meta combines automatically).

A5. Getting the post ID

Field (on AdCreative)Meaning
object_story_idOnly populated if the creative was made from an existing post. Made from object_story_spec → null.
effective_object_story_idThe reliable field — always resolves to the real post id (organic or dark). Use this one.
effective_instagram_media_idIG equivalent (replaces the old effective_instagram_story_id).

Post id format: {page_id}_{post_id}.

Post id lookups
# Get the post id from an ad
GET /v25.0/<AD_ID>?fields=creative{id,object_story_id,effective_object_story_id,effective_instagram_media_id}
# → creative.effective_object_story_id = "123456789_987654321"

# List posts promotable from a Page (needs pages_manage_ads + ads_management + task ADVERTISE)
GET /v25.0/<PAGE_ID>/ads_posts?fields=id,message,created_time,permalink_url&limit=25
# or plain organic posts:
GET /v25.0/<PAGE_ID>/posts?fields=id,message,created_time,permalink_url

A6. Audiences & Targeting

Doc: /reference/custom-audience/, /audiences/.

Custom Audience: POST /act_{id}/customaudiences — name + subtype (CUSTOM general / LOOKALIKE / etc.) + customer_file_source (for customer lists). Upload users: POST /{ca_id}/users with schema + data (SHA-256 identifiers, normalize first: email trim+lowercase, phone digits-only with country code, first/last name lowercase a-z). Max 10k records/request. Delete users via the same edge with DELETE. (⚠️ old subtypes WEBSITE/ENGAGEMENT have likely merged into CUSTOM + rule — verify.)

Lookalike: POST /act_{id}/customaudiences — subtype=LOOKALIKE, origin_audience_id (seed ≥100 members, ideally 1k–5k), lookalike_spec={"type":"similarity"|"reach","country":"ID","ratio":0.01} (ratio 0.01–0.20). Populates in 1–6 hours; check delivery_status.code=200.

Targeting spec (on the ad set): geo_locations (countries/regions/cities/zips), age_min/age_max, genders ([1]=male, [2]=female), interests (search for ids), behaviors, custom_audiences/excluded_custom_audiences, flexible_spec (OR-of-AND), exclusions, publisher_platforms/facebook_positions/instagram_positions, device_platforms. Advantage+ audience: targeting_automation.advantage_audience: 1|0 (from v23.0 onward this must be set explicitly in many cases).

Search interests / estimate reach
# Find an interest id
GET /v25.0/search?type=adinterest&q=Coffee&locale=en_US&limit=10

# Estimate reach
GET /act_<AD_ACCOUNT_ID>/delivery_estimate?optimization_goal=...&targeting_spec={...}

A7. Insights & Reporting

Doc: /insights/. The /insights edge exists on account/campaign/adset/ad — same parameters throughout.

Key metrics: impressions, reach, frequency, spend, clicks, ctr, cpc, cpm, actions (array {action_type,value} — all conversions/engagement), action_values (currency value per action), cost_per_action_type (this is how you get CPA/CPL per action_type), purchase_roas (array — this is ROAS). ⚠️ results/cost_per_result are not API fields — they're a UI derivation (read optimization_goal → map to action_type).

Params: fields, level (account/campaign/adset/ad), date_preset (last_7d/last_30d/…) or time_range={"since","until"}, time_increment, breakdowns (age/gender/publisher_platform/platform_position/country/…), action_attribution_windows / use_unified_attribution_setting.

Derive: ROAS = purchase_roas[omni_purchase].value. CPA = cost_per_action_type[<action_type>].value. CTR = request ctr directly.

⚠️
The action_type key differs per surface (omni_purchase vs offsite_conversion.fb_pixel_purchase vs lead) — inspect the real actions array, don't hardcode blindly.
ROAS + spend + CPA, 7 days
GET /v25.0/<CAMPAIGN_ID>/insights?fields=spend,purchase_roas,actions,cost_per_action_type&date_preset=last_7d&level=campaign

Large reports → async: POST /{id}/insights → report_run_id → poll GET /{report_run_id} until async_status=Job Completed → GET /{report_run_id}/insights. (report_run_id expires after 30 days.)

Part B — CPAS / Collaborative Ads

How this differs from regular ops, and what it adds on top.

B0. What's different

CPAS (Collaborative Ads) = a brand advertises using a catalog owned by a retailer/marketplace (e.g. a third-party e-commerce platform). The retailer shares part of its catalog (a catalog segment) to the brand; the brand runs a catalog-sales campaign against that segment; conversion signal comes from the retailer's pixel, joined via retailer_id. This is how a brand advertises products whose storefront it doesn't own.

Built on top of Advantage+ catalog ads (B2). Main differences from regular ops: (1) the catalog belongs to another party (a segment), (2) attribution comes from the retailer's pixel, (3) many onboarding steps are UI-gated, (4) the brand has restrictions on editing creative/segment.

B1. Roles & onboarding

  • Retailer/Merchant — owns the parent catalog + pixel; creates & shares catalog segments.
  • Brand (advertiser) — accepts ToS, creates a dedicated ad account per retailer, runs campaigns.
  • Marketing Partner/Agency — bridges brand↔retailer discovery.
⚠️
Retail-partner directory approval, accepting ToS in the Collaboration Center, and creating a dedicated ad account are Business Manager/UI operations, not fully API-driven. The API covers: creating segments, sharing segments, and collaboration requests.

B2. Catalog + Advantage+ catalog ads (foundation for both CPAS and non-CPAS)

  • Catalog: POST /{business_id}/owned_product_catalogs — name, vertical (commerce default). ProductItem key = retailer_id (SKU, join key).
  • Product Set: POST /{catalog_id}/product_sets — name + a filter JSON (operators i_contains/contains/eq/is_any + and/or). ⚠️ VERIFY the exact filter schema.
  • Feed: POST /{catalog_id}/product_feeds (scheduled URL) or Catalog Batch API /items_batch.
  • Advantage+ catalog campaign: objective=PRODUCT_CATALOG_SALES (or ODAX OUTCOME_SALES) + promoted_object={"product_catalog_id":"<CATALOG_ID>"}.
  • Ad Set: promoted_object={"product_set_id":"<SET_ID>","custom_event_type":"PURCHASE"}. Retargeting → add product_audience_specs (inclusions/exclusions by event). Prospecting/DABA → omit it.
  • Dynamic creative: object_story_spec.template_data + tokens like {{product.name}}/{{product.price}} + product_set_id.

B3. CPAS flow (retailer → brand)

CPAS setup
# RETAILER: create a catalog segment from the compliant parent catalog
POST /v25.0/{RETAILER_BUSINESS_ID}/owned_product_catalogs
  -F 'name=BrandX Segment' -F 'parent_catalog_id=<RETAILER_PARENT_CATALOG_ID>'
  -F 'catalog_segment_filter={"brand":{"i_contains":"BrandX"}}'
# → segment id (is_catalog_segment=true)

# RETAILER: share the segment to the brand's Business Manager
POST /v25.0/{CATALOG_SEGMENT_ID}/agencies
  -F 'business=<BRAND_BUSINESS_ID>' -F 'permitted_tasks=["ADVERTISE"]'
  -F 'enabled_collab_terms=["ENFORCE_CREATE_NEW_AD_ACCOUNT","ENFORCE_SHARE_AD_PERFORMANCE_ACCESS"]'

# BRAND: campaign uses the SEGMENT id as product_catalog_id (in the retailer-dedicated ad account)
POST /v25.0/act_<BRAND_AD_ACCOUNT_ID>/campaigns
  -F 'objective=PRODUCT_CATALOG_SALES' -F 'promoted_object={"product_catalog_id":"<CATALOG_SEGMENT_ID>"}'
# BRAND: ad set uses a product_set FROM the segment + custom_event_type; creative+ad same as B2.

Discovery (partner/brand): GET /{business_id}?fields=collaborative_ads_suggested_partners, GET /collaborative_ads_directory?fields=collaborative_ads_merchants, POST /{business_id}/collaborative_ads_collaboration_requests.

⚠️

VERIFY: legacy CPAS edge names

The older edge names cpas_business_setup/cpas_advertiser_partnership/cpas_collaboration_request are not confirmed live — what's confirmed is the collaborative_ads_* edges + /agencies shown above. Don't code against cpas_* without checking the live reference.

B4. Brand restrictions (CPAS)

A brand cannot: edit the segment; change multi_share_end_card/description in template_data; use template_url_spec pointing to a non-merchant website. The segment & pixel are controlled by the retailer.

B5. CPAS Insights

CPAS-specific fields: catalog_segment_value, catalog_segment_value_omni_purchase_roas, catalog_segment_actions, converted_product_value, converted_product_quantity. Per-product breakdown: action_converted_product_id (⚠️ not supported at ad-account level — use campaign/adset/ad level).

Part C — Core (auth, batch, rate limits, errors, versioning)

C1. Token & scope

  • System User token (no expiry) = recommended for production. Created in Business Manager → System Users (UI, not API).
  • Long-lived user token (~60 days): GET /v25.0/oauth/access_token?grant_type=fb_exchange_token&client_id=&client_secret=&fb_exchange_token=<short> (server-side, requires app secret).
  • Scopes: ads_management, ads_read, business_management, catalog_management, + pages_* for Page assets. ads_management/business_management need App Review/Advanced Access for accounts you don't own.

C2. Batch & pagination

  • Batch: POST /v25.0/ with batch=[{method,relative_url,body,name,depends_on}]. Max 50 sub-requests. Each sub-request still counts against rate limits individually. Can chain with {result=name:$.id} + depends_on.
  • Pagination: cursor-based — follow paging.next until absent. Don't persist the after cursor across sessions.
  • Ad review is async: after creating an ad, status is PENDING_REVIEW → poll effective_status.

C3. Rate limits (BUC — Business Use Case)

Header X-Business-Use-Case-Usage (Marketing API using a system/page token): {biz_id:[{type,call_count,total_cputime,total_time,estimated_time_to_regain_access}]}. All values are hourly quota percentages.

  • type: ads_insights, ads_management, custom_audience, etc. Standard-access formula (example): Ads Management 300 + 40×active_ads/hour; Insights 600 + 400×active_ads/hour.
  • Strategy: back off BEFORE hitting 100%. If throttled, stop — continuing to hit the API only extends the penalty. Transient rate-limit codes: 4,17,32,613,80000-80004 → retry with exponential backoff. Auth/permission codes (190,10,200,272) → do not retry, fix the token/scope instead.

C4. Error object

{error:{message, type, code, error_subcode, is_transient, error_user_msg, error_data.blame_field_specs, fbtrace_id}}. Branch on code/error_subcode, never parse message. fbtrace_id is for reporting to Meta. code 190 = invalid token, 100 = invalid param, 80004 = Ads Management BUC limit.

C5. Versioning

Each version lives ≥2 years after the next version ships. Deprecation of a specific field can be as short as 90 days (e.g. Advantage+ audience restrictions). Pin v25.0; watch the changelog.

Part D — Cookbook (ready-to-use test cases)

All examples use v25.0, an ads_management token, and <AD_ACCOUNT_ID> without the act_ prefix. USD examples are in cents. Always read → compute → write → readback.

D1. Scale up budget +30% (CBO campaign)

text
GET  /v25.0/<CAMPAIGN_ID>?fields=daily_budget      # "10000"
POST /v25.0/<CAMPAIGN_ID>  daily_budget=13000
GET  /v25.0/<CAMPAIGN_ID>?fields=daily_budget      # confirm 13000

D2. Scale down budget -30% (ABO adset)

Same pattern, POST to <ADSET_ID>, 5000 → 3500.

D3. Pause / Resume

POST /<ID> status=PAUSED / status=ACTIVE. Check effective_status (watch for CAMPAIGN_PAUSED).

D4. Get a post ID from an ad

text
GET /<AD_ID>?fields=creative{effective_object_story_id}   # → split on "_"

D5. Build an ad from an existing post

text
POST /act_<ID>/adcreatives object_story_id=<PAGE>_<POST>
POST /act_<ID>/ads adset_id=..&creative={"creative_id":".."}&status=PAUSED

D6. ROAS/CPA, last 7 days

text
GET /<CAMPAIGN_ID>/insights?fields=spend,purchase_roas,cost_per_action_type&date_preset=last_7d

D7. 1% Lookalike, Indonesia

text
POST /act_<ID>/customaudiences subtype=LOOKALIKE origin_audience_id=<SEED>
  lookalike_spec={"type":"similarity","country":"ID","ratio":0.01}

D8. CPAS setup

See B3 — retailer creates+shares a segment, then brand campaigns against the segment id.

D9. CPAS insights

text
GET /<CAMPAIGN_ID>/insights?fields=catalog_segment_value,catalog_segment_value_omni_purchase_roas,converted_product_quantity

Appendix — ⚠️ VERIFY points before production

  • Minimum budget per billing_event/currency — computed dynamically by Meta, no fixed table. Read error_user_msg, don't hardcode.
  • The "20% learning reset" rule — a heuristic, not an API contract.
  • attachment_hash — not in the v25.0 reference (use image_hash). Check older Kanzen Ads code for it.
  • Old IG fields (instagram_actor_id/instagram_story_id) — deprecated Sept 2025, use instagram_user_id/effective_instagram_media_id.
  • Old CPAS edges (cpas_*) — use the confirmed collaborative_ads_* + /agencies.
  • Product-set filter JSON and the optimization_goal×billing_event matrix — verify live.
  • action_type keys (e.g. omni_purchase) — inspect the real actions array per account.
ℹ️
Compiled from the official Meta Marketing API v25.0 reference. Keep this page and the source guide (mcp/meta-marketing-api-guide.md) in sync when Meta ships a new API version.
← API Reference