Meta
Overview
The Meta Core API mirrors Meta's Graph API 1:1 — ad accounts, campaigns, ad sets, ads, creatives, insights, Pages and granted permissions.
| Base path | /api/core/meta |
| Graph version | v25.0 |
| Auth | the calling organization's own stored Meta access token, resolved from its meta_ads integration. You never pass a Meta token yourself |
| Every request | requires ?organizationId=<uuid> |
Every query param except organizationId is forwarded to the Graph API verbatim. fields, limit, after, time_range, time_increment, breakdowns, filtering, level, action_attribution_windows — all keep Meta's own names and semantics. Analify renames nothing and defaults nothing, so Meta's own reference is the field-level source of truth.
Dry-running a create call
Every campaign/adset/ad/creative/audience/rule/experiment create endpoint already accepts Meta's own execution_options: ["validate_only"] field in the POST body — since these routes are pure pass-through, this has always worked. Meta validates the object (auth, field shape, budget/targeting rules) and returns success/failure without creating anything live. Use it before a scripted bulk-launch, or any time an agent is about to spend real budget on a config it hasn't run before.
API Endpoints
49 routes. Insights edges on Pages, Posts and Instagram require Meta's metric param; Instagram metrics differ by media type.
| Endpoint | Meta Real Endpoint | Description |
|---|---|---|
GET /accounts | GET /me/adaccounts | Ad accounts the token can reach |
GET /accounts/{accountId} | GET /{accountId} | Account currency + timezone |
POST /accounts/{accountId} | POST /{accountId} | Update ad-account settings (spend_cap, name, notifications_enabled) |
GET /accounts/{accountId}/insights | GET /{accountId}/insights | Account-level insights |
GET /accounts/{accountId}/delivery-estimate | GET /{accountId}/delivery_estimate | Audience reach sizing before creating an ad set |
GET /accounts/{accountId}/campaigns | GET /{accountId}/campaigns | Campaigns under an account |
POST /accounts/{accountId}/campaigns | POST /{accountId}/campaigns | Create a campaign |
GET /accounts/{accountId}/adsets | GET /{accountId}/adsets | Ad sets under an account |
POST /accounts/{accountId}/adsets | POST /{accountId}/adsets | Create an ad set |
GET /accounts/{accountId}/ads | GET /{accountId}/ads | Ads under an account |
POST /accounts/{accountId}/ads | POST /{accountId}/ads | Create an ad |
GET /accounts/{accountId}/creatives | GET /{accountId}/adcreatives | Ad creatives under an account |
POST /accounts/{accountId}/creatives | POST /{accountId}/adcreatives | Create an ad creative |
POST /accounts/{accountId}/images | POST /{accountId}/adimages | Upload an ad image (bytes base64, or image_url — Analify fetches an already-hosted asset server-side) |
POST /accounts/{accountId}/videos | POST /{accountId}/advideos | Upload an ad video (hosted-file file_url) |
GET /accounts/{accountId}/audiences | GET /{accountId}/customaudiences | List Custom Audiences / Lookalikes |
POST /accounts/{accountId}/audiences | POST /{accountId}/customaudiences | Create a Custom Audience or Lookalike |
GET /accounts/{accountId}/rules | GET /{accountId}/adrules_library | List Automated Rules |
POST /accounts/{accountId}/rules | POST /{accountId}/adrules_library | Create an Automated Rule |
GET /accounts/{accountId}/experiments | GET /{accountId}/ad_studies | List Split Tests |
POST /accounts/{accountId}/experiments | POST /{accountId}/ad_studies | Create a Split Test |
GET /accounts/{accountId}/activities | GET /{accountId}/activities | Account change/activity log |
GET /accounts/{accountId}/adspixels | GET /{accountId}/adspixels | Pixels on the account |
GET /accounts/{accountId}/dataset-metadata | GET /{accountId}/dataset_metadata | CAPI / offline datasets |
GET /accounts/{accountId}/promote-pages | GET /{accountId}/promote_pages | Pages this account may promote |
GET /campaigns/{campaignId} | GET /{campaignId} | Get one campaign |
POST /campaigns/{campaignId} | POST /{campaignId} | Update a campaign |
DELETE /campaigns/{campaignId} | DELETE /{campaignId} | Delete a campaign permanently |
GET /campaigns/{campaignId}/insights | GET /{campaignId}/insights | Campaign insights |
GET /adsets/{adSetId} | GET /{adSetId} | Get one ad set |
POST /adsets/{adSetId} | POST /{adSetId} | Update an ad set |
DELETE /adsets/{adSetId} | DELETE /{adSetId} | Delete an ad set permanently |
GET /adsets/{adSetId}/insights | GET /{adSetId}/insights | Ad-set insights |
GET /ads/{adId} | GET /{adId} | Get one ad |
POST /ads/{adId} | POST /{adId} | Update an ad |
DELETE /ads/{adId} | DELETE /{adId} | Delete an ad permanently |
POST /ads/{adId}/copies | POST /{adId}/copies | Duplicate an ad |
GET /ads/{adId}/insights | GET /{adId}/insights | Ad-level insights |
GET /ads/{adId}/previews | GET /{adId}/previews | Rendered ad preview |
GET /creatives/{creativeId} | GET /{creativeId} | Get one ad creative |
DELETE /creatives/{creativeId} | DELETE /{creativeId} | Delete an ad creative permanently |
GET /audiences/{audienceId} | GET /{audienceId} | Get one audience |
DELETE /audiences/{audienceId} | DELETE /{audienceId} | Delete an audience |
POST /audiences/{audienceId}/users | POST /{audienceId}/users | Add hashed customer records to a Custom Audience |
GET /rules/{ruleId} | GET /{ruleId} | Get one Automated Rule |
POST /rules/{ruleId} | POST /{ruleId} | Update an Automated Rule (e.g. enable/disable) |
DELETE /rules/{ruleId} | DELETE /{ruleId} | Delete an Automated Rule |
GET /experiments/{studyId} | GET /{studyId} | Get one Split Test |
POST /experiments/{studyId} | POST /{studyId} | Update a Split Test (e.g. stop it) |
POST /pixels/{pixelId}/events | POST /{pixelId}/events | Conversions API — send server-side events |
GET /objects | GET /?ids= | Batch read many objects at once |
GET /pages | GET /me/accounts | Facebook Pages the token manages |
GET /permissions | GET /me/permissions | Scopes actually granted to the token |
GET /businesses | GET /me/businesses | Business Managers reachable |
GET /businesses/{businessId}/owned-product-catalogs | GET /{businessId}/owned_product_catalogs | Catalogs of one business |
GET /owned-product-catalogs | GET /me/owned_product_catalogs | Catalogs reachable directly |
GET /catalogs/{catalogId}/products | GET /{catalogId}/products | Products as Meta holds them |
GET /pages/{pageId} | GET /{pageId} | Page profile |
GET /pages/{pageId}/insights | GET /{pageId}/insights | Organic Page performance |
GET /pages/{pageId}/feed | GET /{pageId}/feed | Full timeline |
GET /pages/{pageId}/posts | GET /{pageId}/posts | Posts by the Page |
GET /posts/{postId}/comments | GET /{postId}/comments | Comment thread |
GET /posts/{postId}/insights | GET /{postId}/insights | Per-post reach |
GET /videos/{videoId} | GET /{videoId} | Video metadata |
GET /instagram/{igUserId} | GET /{igUserId} | IG Business profile |
GET /instagram/{igUserId}/media | GET /{igUserId}/media | Posts, reels, stories |
GET /instagram/{igUserId}/insights | GET /{igUserId}/insights | IG account performance |
GET /instagram/media/{mediaId}/insights | GET /{mediaId}/insights | Per-post IG performance |
Accounts
GET /api/core/meta/accounts
GET /api/core/meta/accountsEvery ad account the organization's token can reach. Use this to discover the {accountId} (Meta's act_<id> form) the routes below take.
curl "https://analify.scalyax.ai/api/core/meta/accounts?organizationId={organizationId}&fields=id,name,currency,account_status" \
-H "Authorization: Bearer {anlfy_token}"Which account is used? Routes that do not name an account in the path fall back to the ad account stored on the integration. The /accounts/{accountId}/* routes let you target any account the token can reach — pass the id in the path.
GET /api/core/meta/accounts/{accountId}/insights
GET /api/core/meta/accounts/{accountId}/insightsThe workhorse reporting endpoint. Takes Meta's own insights params — fields, level (account/campaign/adset/ad), time_range={"since":"YYYY-MM-DD","until":"YYYY-MM-DD"}, time_increment, breakdowns, action_attribution_windows, filtering.
curl -G "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/insights" \
-H "Authorization: Bearer {anlfy_token}" \
--data-urlencode "organizationId={organizationId}" \
--data-urlencode "level=campaign" \
--data-urlencode "fields=campaign_name,spend,impressions,actions,purchase_roas" \
--data-urlencode 'time_range={"since":"2026-08-01","until":"2026-08-16"}'purchase_roas here is Meta's own reported ROAS. It is not Analify's nROAS after RTO and, for a COD store with returns, the two differ materially. Use the Analytics API when you want the profit-true number; use Core API when you want exactly what Meta says.
GET /api/core/meta/accounts/{accountId}/delivery-estimate
GET /api/core/meta/accounts/{accountId}/delivery-estimateSize an audience BEFORE creating the ad set that targets it. Pass targeting_spec (JSON-encoded, same shape as an ad set's targeting) and optimization_goal; response includes estimate_ready, estimate_mau_lower_bound, estimate_mau_upper_bound.
POST /api/core/meta/accounts/{accountId}
POST /api/core/meta/accounts/{accountId}Update ad-account-level settings — Meta's own fields, forwarded verbatim.
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"spend_cap": 50000, "notifications_enabled": true}'Campaigns, ad sets and ads
Reads take fields; writes POST Meta's own field names in a JSON body.
# Read one campaign
curl "https://analify.scalyax.ai/api/core/meta/campaigns/23851234567890123?organizationId={organizationId}&fields=id,name,status,daily_budget,objective" \
-H "Authorization: Bearer {anlfy_token}"
# Pause it — Meta's own `status` field, forwarded as-is
curl -X POST "https://analify.scalyax.ai/api/core/meta/campaigns/23851234567890123?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"status": "PAUSED"}'
# Delete it permanently — distinct from status: DELETED, which archives it for reporting
curl -X DELETE "https://analify.scalyax.ai/api/core/meta/campaigns/23851234567890123?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}"Campaign, ad-set, ad and creative writes change a live ad account — budgets and delivery are affected immediately. They require an API token with the write scope. DELETE is available on campaigns, ad sets, ads and creatives.
Ad-set updates work the same way (POST /api/core/meta/adsets/{adSetId}) and accept Meta's ad-set fields, e.g. daily_budget, bid_amount, status, targeting. Ad updates (POST /api/core/meta/ads/{adId}) accept name, status, creative.
Creating campaigns, ad sets, ads and creatives
These routes launch new objects rather than editing existing ones — same rule as everywhere else in this API: Meta's own field names, forwarded verbatim, no reshaping. Add "execution_options": ["validate_only"] to any of these bodies to validate without creating anything live.
# Create a campaign
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/campaigns?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"name": "Traffic Campaign", "objective": "OUTCOME_TRAFFIC", "status": "PAUSED", "special_ad_categories": []}'
# Create an ad set under that campaign
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/adsets?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"campaign_id": "23851234567890123", "name": "Traffic AdSet", "billing_event": "LINK_CLICKS", "status": "PAUSED", "targeting": {"age_min": 18, "age_max": 65, "geo_locations": {"countries": ["EG"]}}}'
# Upload an image from local bytes (base64) OR from an already-hosted URL — pick whichever you have
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/images?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"bytes": "<base64-encoded-image-data>"}'
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/images?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"image_url": "https://cdn.example.com/product-photo.jpg"}'
# Create a single-image creative using that hash
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/creatives?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"name": "Single Image Creative", "object_story_spec": {"page_id": "111", "link_data": {"link": "https://example.com", "message": "Ad text", "image_hash": "abc123"}}}'
# Create the ad from that creative
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/ads?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"name": "Test Ad", "adset_id": "23860001", "status": "PAUSED", "creative": {"creative_id": "23870001"}}'
# Duplicate an existing ad instead of building a new one
curl -X POST "https://analify.scalyax.ai/api/core/meta/ads/23870001/copies?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"status_option": "PAUSED", "rename_options": {"rename_strategy": "ONLY_TOP_LEVEL_RENAME"}}'Image upload accepts two sources — bytes (base64, e.g. a file read locally or through an MCP client) or image_url (Analify fetches an already-hosted asset — a Shopify product photo, a rendered creative — server-side and forwards it to Meta; capped at Meta's 10MB ad-image limit). Video upload only supports the hosted-file variant — {"file_url": "https://..."} — not chunked/binary upload. Creatives also accept carousel (link_data.child_attachments), catalog/DPA (template_data + product_set_id), and Meta's GenAI creative features (degrees_of_freedom_spec — text generation, image uncrop, background generation) — see Meta's Ad Creative reference for the full field set; every field is forwarded as-is.
Audiences (Custom Audiences + Lookalikes)
Retargeting and prospecting — the same customaudiences endpoint creates both a Custom Audience (customer list, pixel/website) and a Lookalike, distinguished by subtype.
# Create a Custom Audience
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/audiences?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"name": "Website visitors 30d", "subtype": "WEBSITE", "customer_file_source": "USER_PROVIDED_ONLY"}'
# Add hashed customer records to it
curl -X POST "https://analify.scalyax.ai/api/core/meta/audiences/{audienceId}/users?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"payload": {"schema": ["EMAIL_SHA256"], "data": [["<sha256-hash>"]]}}'
# Create a Lookalike from an existing audience
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/audiences?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"name": "LAL 1% Egypt", "subtype": "LOOKALIKE", "origin_audience_id": "{audienceId}", "lookalike_spec": {"type": "similarity", "ratio": 0.01, "country": "EG"}}'
Member data must be pre-hashed (SHA-256) by the caller.POST .../usersnever hashes or inspects the payload, and Meta rejects unhashed PII with its own validation error. This endpoint's body is never logged by Analify's tracing layer, but you are still responsible for hashing correctly before sending it. Removing members (DELETE .../userswith a body) is not yet exposed.
Conversions API (server-side events)
POST /api/core/meta/pixels/{pixelId}/events sends server-side conversion events — the resilient counterpart to browser-side Pixel tracking after iOS14.5/ATT signal loss. Find a pixel id via GET /accounts/{accountId}/adspixels.
curl -X POST "https://analify.scalyax.ai/api/core/meta/pixels/{pixelId}/events?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"data": [{"event_name": "Purchase", "event_time": 1735689600, "action_source": "website", "user_data": {"em": ["<sha256-hash>"], "client_ip_address": "1.2.3.4"}, "custom_data": {"value": 199.99, "currency": "EGP"}}]}'user_data may carry hashed PII (em/ph) alongside unhashed fields Meta's own spec allows (client_ip_address, client_user_agent, fbc, fbp) — same never-logged guarantee as Audiences above.
Automated Rules
Schedule-evaluated conditions — "if CPA > X for 3 days, pause" — that Meta acts on without polling.
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/rules?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"name": "Pause high CPA campaigns", "evaluation_spec": {"evaluation_type": "SCHEDULE", "filters": [{"field": "cpa", "operator": "GREATER_THAN", "value": 5000}]}, "execution_spec": {"execution_type": "PAUSE"}, "schedule_spec": {"schedule_type": "DAILY"}}'
# Disable a rule
curl -X POST "https://analify.scalyax.ai/api/core/meta/rules/{ruleId}?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"status": "DISABLED"}'Split Tests / Experiments
A controlled A/B test between cells of campaigns, instead of eyeballing insights across two campaigns run at different times.
curl -X POST "https://analify.scalyax.ai/api/core/meta/accounts/act_123456789/experiments?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}" -H "Content-Type: application/json" \
-d '{"name": "Creative A/B", "type": "SPLIT_TEST", "cells": [{"name": "Video", "treatment_percentage": 50, "campaign_group_ids": ["{campaignId}"]}, {"name": "Image", "treatment_percentage": 50, "campaign_group_ids": ["{campaignId}"]}]}'Batch reads
GET /api/core/meta/objects
GET /api/core/meta/objectsMeta's batch object read — one call, many ids, one fields set. Cheaper than N single reads and counts as one Graph call against your rate budget.
curl "https://analify.scalyax.ai/api/core/meta/objects?organizationId={organizationId}&ids=23851111,23852222&fields=id,name,status" \
-H "Authorization: Bearer {anlfy_token}"Creatives and previews
GET /api/core/meta/creatives/{creativeId} returns the raw creative (object_story_spec, asset_feed_spec, image/video ids, …). GET /api/core/meta/ads/{adId}/previews returns Meta's rendered preview — pass Meta's ad_format (e.g. DESKTOP_FEED_STANDARD, INSTAGRAM_STORY).
Pages and permissions
GET /api/core/meta/permissions
GET /api/core/meta/permissionsThe scopes Meta actually granted this token. Check this first when a call returns a 403 — a missing scope, not a bug in the route, is the usual cause.
curl "https://analify.scalyax.ai/api/core/meta/permissions?organizationId={organizationId}" \
-H "Authorization: Bearer {anlfy_token}"GET /api/core/meta/pages mirrors GET /me/accounts — the Facebook Pages the token manages, each with its own page access token and tasks.
Error Shape
Meta's Graph API collapses every failure into a body error code rather than an HTTP status, so Core API maps it back onto a real status:
| Condition | HTTP status |
|---|---|
| Token invalid / expired | 401 |
| Missing permission (scope) | 403 |
| Object not found | 404 |
| Rate limited | 429 |
| Anything else | 400 |
Graph's own code and subcode are preserved under error.details:
{
"success": false,
"error": {
"message": "(#100) Unsupported get request. Object with ID 'act_000' does not exist",
"code": "META_API_ERROR",
"details": { "code": 100, "subcode": 33 }
}
}Before any Graph call is attempted, credential problems surface as:
| Code | Status | Meaning |
|---|---|---|
META_NOT_CONNECTED | 400 | The organization has no connected Meta integration |
META_RECONNECT_REQUIRED | 401 | The stored token is dead — the user must reconnect Meta |
META_CREDENTIALS_ERROR | 400 | Credential resolution failed for another reason |
| META_UNKNOWN_ERROR | 500 | A non-Graph failure inside the route |
Updated about 1 month ago
