# Meta Marketing API Proxy over the Meta Graph / Marketing API: reading the ad account structure (ad accounts, campaigns, ad sets, ads, creatives) and insights (spend, impressions, clicks, conversions). **This phase is read-only.** Every endpoint issues a GET upstream; campaign management is phase 2 (see `overview.md`, "Deliberately not wired"). Base URL: `https://graph.facebook.com/{version}` — the version comes from `META_API_VERSION` (default `v25.0`) and can be overridden per request with `X-Meta-Api-Version`, so a Graph upgrade needs no deploy. ## Credentials | Header | Required | Meaning | | --- | --- | --- | | `X-Meta-Access-Token` | one of these | Access token → sent upstream as `Authorization: Bearer`. | | `Authorization` | one of these | Standard `Authorization: Bearer ` — equivalent alternative (the X- header wins if both are sent). | | `X-Meta-App-Secret` | no, recommended | App secret; the proxy derives `appsecret_proof` per request. | | `X-Meta-Api-Version` | no | Graph version override, e.g. `v25.0`. | > Use a **System User** token from Business Manager. Unlike a user OAuth token > it does not stop working when someone leaves the company or changes their > password, and when generated without an expiry it needs no refreshing at all. Required permissions for this read-only phase: `ads_read`, `business_management`. ## Endpoints ### Structure | Method | Path | Purpose | | --- | --- | --- | | GET | `/ads/me/adaccounts` | Ad accounts the token can access. | | GET | `/ads/me/businesses` | Business Manager accounts the token can access. | | GET | `/ads/businesses/{business_id}/adaccounts` | Accounts owned by a business (`owned=false` → accounts shared with it). | | GET | `/ads/accounts/{account_id}` | Ad account detail. | | GET | `/ads/accounts/{account_id}/campaigns` | Campaigns in an account. | | GET | `/ads/accounts/{account_id}/adsets` | Ad sets in an account. | | GET | `/ads/accounts/{account_id}/ads` | Ads in an account. | | GET | `/ads/accounts/{account_id}/adcreatives` | Creatives in an account. | | GET | `/ads/campaigns/{campaign_id}` | Campaign detail. | | GET | `/ads/campaigns/{campaign_id}/adsets` | Ad sets in a campaign. | | GET | `/ads/campaigns/{campaign_id}/ads` | Ads in a campaign. | | GET | `/ads/adsets/{adset_id}` | Ad set detail. | | GET | `/ads/adsets/{adset_id}/ads` | Ads in an ad set. | | GET | `/ads/ads/{ad_id}` | Ad detail. | | GET | `/ads/adcreatives/{creative_id}` | Creative detail. | `account_id` is accepted with or without the `act_` prefix. Every list endpoint takes `fields` (comma-separated Graph fields, with a useful default), `limit`, `after` (cursor) and `all_pages` (auto-paging, on by default — see [Pagination](#pagination)). The account-level campaign / adset / ad endpoints also take `effective_status`, a JSON array such as `["ACTIVE","PAUSED"]`. ### Insights | Method | Path | Purpose | | --- | --- | --- | | GET | `/ads/insights/{object_id}` | Synchronous insights — small reports. | | POST | `/ads/insights/{object_id}/jobs` | Start an async report job → `report_run_id`. | | GET | `/ads/insights/jobs/{report_run_id}` | Job status (`async_status`, `async_percent_completion`). | | GET | `/ads/insights/jobs/{report_run_id}/results` | Read a finished job. | | POST | `/ads/insights/{object_id}/run` | Start, wait for completion, return the rows. | `object_id` is an ad account (`act_123`), campaign, ad set or ad id. Parameters (identical across all of them): `fields`, `level`, `date_preset`, `time_range`, `time_increment`, `breakdowns`, `action_breakdowns`, `filtering`, `sort`. **Sync or async?** Meta computes large reports asynchronously and will reject or time out a sync call that is too big. Rule of thumb: one account or a handful of campaigns over a short period → `GET`. Long periods, several breakdowns, or a whole account at ad level → `/run`. `/run` polls until the job finishes or the wait budget (`META_ASYNC_MAX_WAIT_SECONDS`, default 120 s, overridable per call with `max_wait_seconds`) runs out. On timeout the job is **not** cancelled — the response carries `completed: false` and the `report_run_id`, so you can keep polling `/ads/insights/jobs/{report_run_id}` instead of losing the work. #### Fields and levels The default `fields` list is deliberately level-agnostic: ``` spend,impressions,clicks,ctr,cpc,cpm,reach,frequency,actions,action_values,date_start,date_stop ``` Name/id fields are level-specific — asking for `ad_id` at campaign level is an upstream error. Add them together with `level`: - `level=campaign` → `campaign_id,campaign_name,…` - `level=adset` → `adset_id,adset_name,campaign_name,…` - `level=ad` → `ad_id,ad_name,adset_name,campaign_name,…` Conversions live in `actions` / `action_values` (arrays keyed by `action_type`), not in a top-level metric. ### Generic read passthrough | Method | Path | Purpose | | --- | --- | --- | | GET | `/graph/{graph_path}` | Any Graph API GET. | For everything the typed endpoints do not cover (custom audiences, ad rules, Instagram accounts, Pages…) without needing a deploy. GET-only by construction, so the read-only stance holds here too. Give the path **without** the version prefix — the version comes from config or `X-Meta-Api-Version`. Unknown query params are forwarded verbatim. ## Pagination The proxy **follows `paging.next` for you by default** (`all_pages=true`), so a list call returns the complete list: ```json { "data": [ ... ], "pages_read": 3, "truncated": false } ``` `truncated: true` means the `META_MAX_PAGES` cap (default 100) was hit; the response also carries the `next` URL and a warning is logged. Graph's own page size defaults to 25 — pass a bigger `limit` (up to 500 on most edges) to fetch more rows in fewer round trips. Pass `all_pages=false` to get a single raw page with the upstream `paging.cursors` untouched and drive the cursor yourself. ## Rate limits `X-App-Usage`, `X-Ad-Account-Usage` and `X-Business-Use-Case-Usage` from the upstream response are copied onto our response — use them to pace calls. ## Errors | Status | Meaning | | --- | --- | | 401 `missing_credentials` | No token, or a malformed `X-Meta-Api-Version`. | | 400 `upstream_error` | Meta rejected the request (bad field, bad id, missing permission). | | 502 / 504 | Meta unreachable, 5xx, or a request timeout. | Upstream errors keep Meta's status and the whole error object in `upstream_body`, including `code`, `error_subcode` and `fbtrace_id` (quote it when opening a case with Meta). `detail` prefers Meta's human-readable `error_user_msg` when present. Common ones: - **code 100, "Invalid appsecret_proof"** — the app enforces app-secret proof; send `X-Meta-App-Secret`. - **code 190** — token expired or revoked (a user OAuth token after a password change; a System User token generated with an expiry). - **code 200** — the token lacks `ads_read` on that ad account. - **code 17 / 613** — rate limited; check the usage headers and back off. ## Kde získat údaje (návod pro klienta) - **Access token (System User)**: [Business Manager](https://business.facebook.com/) → *Nastavení firmy → Uživatelé → Systémoví uživatelé* → **Přidat** → **Přidat aktiva** (vyberte reklamní účty, oprávnění *Správa kampaní*) → **Vygenerovat nový token**. Bez zadané expirace je token trvalý. Oprávnění pro tuto fázi: `ads_read`, `business_management`. - **App secret**: [developers.facebook.com](https://developers.facebook.com/apps/) → vaše aplikace → *Nastavení → Základní → App Secret*. Povinný, pokud má aplikace zapnuté *Require app secret proof for server API calls*. - **ID reklamního účtu**: Business Manager → *Nastavení firmy → Reklamní účty*, nebo vlevo nahoře v Ads Manageru. Prefix `act_` je volitelný. ## curl examples Campaigns in an account: ```bash curl "https://services.csbot.cz/apps/meta/ads/accounts/123456789/campaigns?limit=200" \ -H "X-Meta-Access-Token: EAAG..." \ -H "X-Meta-App-Secret: " ``` Daily campaign insights for June, run asynchronously: ```bash curl -X POST "https://services.csbot.cz/apps/meta/ads/insights/act_123456789/run\ ?level=campaign\ &fields=campaign_id,campaign_name,spend,impressions,clicks,ctr,actions\ &time_range=%7B%22since%22%3A%222026-06-01%22%2C%22until%22%3A%222026-06-30%22%7D\ &time_increment=1" \ -H "X-Meta-Access-Token: EAAG..." \ -H "X-Meta-App-Secret: " ``` Anything not covered by a typed endpoint: ```bash curl "https://services.csbot.cz/apps/meta/graph/act_123456789/customaudiences?fields=id,name,approximate_count" \ -H "X-Meta-Access-Token: EAAG..." ```