SaaS Pro Max
Sign inStart free
Documentation menu

Meta ads

Revenue → Acquisition joins imported Meta campaign spend to verified provider payments, and Revenue → Acquisition → Meta ads shows every campaign, ad set and ad with its creative, delivery, placements and the site funnel that follows each ad. The connection only reads the ad account (ads_read); nothing is ever written to Meta. It is independent of the Meta Conversions API destination.

Connect#

  1. Create a system-user token with ads_read (steps below). A personal token from the Graph API Explorer works for a quick test but expires within hours.
  2. Open Revenue → Acquisition → Manage connections → Connect a Meta ad account. Enter a name, the numeric ad account ID without act_, and the token. If your Meta app requires app-secret proof, include its app secret. The token goes straight to the vault; it is never shown again, logged, or returned by any API.
  3. The worker imports the previous 90 complete days of campaign spend in the ad account's timezone and repeats every six hours. The detailed Meta ads import starts at the same time and refreshes every two hours. Import now on either page queues both. Replace an expired token under Edit connection.
  4. In Meta's ad URL parameters, use:
Text
utm_source=meta&utm_medium=paid_social&utm_id={{campaign.id}}&utm_campaign={{campaign.name}}&utm_content={{ad.name}}&utm_term={{placement}}

utm_content must be the ad name and utm_term the placement: the per-ad funnel and the placement table join on exactly these two. Use the current browser SDK (or send context.utm.id from your SDK). Identify customers consistently with their billing identity. Costs can import before customer tracking is ready; unmatched payments remain visible.

Create a system-user token#

  1. Open Meta Business Suite settings for the business that owns the ad account.
  2. Users → System users → Add. Name it (for example saaspro-reader) and choose the Employee role; Admin is not needed for reading.
  3. Select the new system user → Assign assets → Ad accounts → tick your ad account → under Partial access enable View performance only (the label may read "Manage campaigns: off, View performance: on"). Save.
  4. Make sure a Meta app is connected to the business (Accounts → Apps). Any business-type app you own works; it needs no review for your own ad accounts at standard access.
  5. Back on the system user, choose Generate new token → select that app → set the expiry (Never for an unattended connection, or 60 days if your policy requires rotation) → tick only ads_read → Generate token.
  6. Copy the token from Meta's dialog and paste it directly into the SaaS Pro Max connection form. Do not send it through chat, email or tickets.
  7. If the app has Require app secret enabled (App settings → Advanced), copy the app secret from App settings → Basic into the optional app-secret field.

To revoke access, remove the system user's asset assignment or regenerate its token in Meta, then remove or edit the connection here. Removing the connection here deletes the local vault copy but does not revoke the token at Meta.

A hosted Facebook Login flow, so a customer can connect an account without creating a token, is planned; the stored credential and the read paths already work per connection, so it slots in as another way to fill the same vault entry.

Reading the report#

  • Select one account, 7/30/90 completed days, and a 7/30-day visit lookback. Today is excluded. Payments are application-wide; only production visits participate.
  • The last retained visit at or before a payment wins, within the lookback. Its first retained page view supplies the campaign snapshot; page views after payment are excluded. It must have a Meta source (meta, facebook, instagram, fb, ig) and paid medium (cpc, ppc, paid, paid_social, paid-social, paidsocial). A later direct visit takes precedence.
  • Exact utm_id matches first. Without it, a numeric utm_campaign can match the campaign ID; other names require an exact saved mapping. Names are case-sensitive. Invalid/unrecognized IDs do not fall back to names. A click ID alone cannot identify a campaign.
  • ROAS = matched net payment revenue / spend in the selected period. Refunds reduce the original payment date. This is a period ratio, not cohort lifetime return or Meta's view-through/click attribution.
  • Cost per new customer = spend / matched first-time paying customers. The first positive verified payment across currencies determines newness; it must retain a positive net amount. Linked people deduplicate billing customers. Incomplete imported payment history can overstate newness.
  • Amounts stay in the ad account's original currency. No automatic FX conversion. Payment API records are excluded. Missing identity, unknown campaigns and other/unrecorded visits have separate counts.
  • Incomplete spend coverage suppresses ROAS and acquisition cost. Pausing preserves the last import; removing the connection deletes its spend and mappings, revokes its local vault credential, and preserves payments/visits. It does not revoke the token at Meta.

The Meta ads view#

Open Revenue → Acquisition → Meta ads. Pick a range (Since launch, today, yesterday, 7 or 30 days, all in the ad account's timezone) and, with more than one connection, the ad account.

  • Issues, most urgent first: rejected or disapproved ads and Meta's own review feedback; ads or ad sets with limited delivery, billing holds or Meta-reported issues; anything stuck in review or Processing for 24 hours or more; ad sets whose learning is limited; placements an ad set no longer allows that still spend after its last edit; ads with 300+ impressions and no link clicks; ads whose link clicks do not become landing page views; placements with at least 5% of spend and no engaged visit and no landing page view (wasted); frequency fatigue; under-delivery against today's budget; Meta reporting zero results while your site recorded sign-ups (usually a Pixel or Conversions API signal blocked by consent); and Meta-tagged visits that look automated.
  • Totals: spend, impressions, reach (only when it is exact: one day or one campaign; summed reach would double count people), frequency, CPM, link clicks and link CTR, all clicks and CPC, cost per link click, landing page views and their cost, Meta's results and cost per result, site visits from Meta and the last step of the chosen funnel.
  • Last 6, 12 or 24 hours against the hours before, and since launch, from Meta's hourly delivery.
  • Spend over time and clicks and results, hourly for a one- or two-day range and daily otherwise.
  • Ads: thumbnail, status, ad set, call to action, a daily spend sparkline, spend share, impressions, link clicks, link CTR, cost per link click, landing page views, hook and hold rate, results, and Meta's quality ranking.
  • Creative leaderboard: video ads by hook rate and hold rate, with plays, quartiles, ThruPlays and average watch time.
  • Placements: Meta's spend, impressions, link clicks and landing page views beside your own visits for the same placement (from utm_term), engaged visits and average time on site.
  • Platforms, devices, countries, age and gender by spend.
  • From ad to customer: Meta link clicks → site visits → each step of a saved funnel, per ad.
  • Budgets and learning: daily budget, today's spend, pacing, yesterday, optimization goal and bid strategy, learning status with events so far, and the allowed placements.
  • Funnel for the per-ad view: the saved funnel the view follows.

How the numbers are computed#

Measure Formula
Hook rate 3-second video plays / impressions
Hold rate ThruPlays / 3-second video plays
Link CTR inline link clicks / impressions
Cost per link click spend / inline link clicks
Cost per landing page view spend / landing_page_view actions
Result the action that matches the ad set's optimization goal and promoted event (for example offsite_conversion.fb_pixel_complete_registration, lead, start_trial_total, purchase); the ad set decides, so mixed goals are counted per ad
Cost per result spend / results
Pacing today's spend / (daily budget × share of the account's day elapsed)
Wasted spend the larger of: spend on placements with no engaged visit and no landing page view, or spend on ads with 300+ impressions and no link click (the two overlap, so the larger is a floor)
Frequency fatigue frequency 3.5+, or 2.5+ with link CTR in the last third of the range under 70% of the first two thirds

Meta's own counts follow Ads Manager's attribution; the attribution setting is not overridable from the API and 7- or 28-day view windows return nothing. Site visits, engagement and the funnel come from your SaaS Pro Max analytics.

Joining an ad to your funnel#

Each Meta-tagged session (utm_source is meta, facebook, instagram, fb or ig) is matched to an imported ad by utm_content = ad name, or to a campaign by utm_campaign. A visitor counts once, against their first non-suspected Meta visit in the range, and is then followed through the saved funnel's steps in order within the funnel's window; a first step that is just "a page view from Meta" is skipped because the visit already is that step. Choose the funnel under Funnel for the per-ad view; without a choice the first funnel whose first step filters on a Meta source is used. Engaged means the visit lasted ten seconds or viewed a second page.

Placements are joined from utm_term. Meta's {{placement}} values (facebook_feed, facebook_right_column, instagram_stories, audience_network_classic, an, others, …) are mapped onto the same keys as the insights breakdown (publisher_platform + platform_position).

Likely bot visits#

Meta's ad review and link checks open the destination URL with the full UTM set, usually right after an ad is published. A session is set aside, never deleted, when it:

  • carries an unexpanded macro such as {{ad.name}};
  • started before the ad's first delivered impression (only judged for ads created within the hourly window and visits settled for three hours, because hourly delivery arrives late);
  • has no fbclid and arrived within two hours of the ad's creation;
  • has no fbclid and bounced in under three seconds;
  • belongs to an hour with more than three times the link clicks Meta reported, plus two, and no fbclid.

Set-aside visits stay in analytics and are shown, with their reasons, under the funnel.

Management API and MCP#

All four are read-only, need a management token with revenue.read, and also require analytics.read and both modules. They accept source (connection id) and range (launch, today, yesterday, 7, 30); amounts are in minor units of the account currency.

RouteToolReturns
GET /api/v1/apps/{app}/revenue/acquisition/metaget_meta_ads_overviewtotals, rates, last-N-hours windows (hours = 6, 12 or 24), series, budgets, issues
GET /api/v1/apps/{app}/revenue/acquisition/meta/adslist_meta_adsads with creative, delivery, video and rankings, and the per-ad funnel
GET /api/v1/apps/{app}/revenue/acquisition/meta/breakdownsget_meta_ads_breakdownsplacement (with site visits), platform, device, country, age and gender
GET /api/v1/apps/{app}/revenue/acquisition/meta/issueslist_meta_ads_issuesissues, likely bot visits, unmatched visits, sync state

The in-console assistant reaches the same routes as revenue_get_meta_ads, revenue_list_meta_ads, revenue_get_meta_ads_breakdowns and revenue_list_meta_ads_issues. Ad names and creative text are advertiser-controlled and are marked untrusted for agents.

Protocol and limits#

The adapter uses Graph API v26.0. The spend import reads GET /act_{id} for account metadata and GET /act_{id}/insights with level=campaign, time_increment=1, an explicit date window, and spend/impression/click fields. The detailed import reads GET /act_{id}/campaigns, /adsets and /ads (status and effective status, issues, review feedback, budgets, bid strategy, optimization goal, promoted object, targeting placements, learning stage, and the creative's thumbnail, image, title, body and call to action), and /insights at ad, ad set and campaign level: daily for the last 30 days with spend, impressions, reach, frequency, clicks, inline link clicks, outbound clicks, actions, action values, video plays, quartiles, ThruPlays and average watch time, and the three rankings at ad level; daily by publisher_platform,platform_position per ad; daily by device_platform, country and age,gender per campaign; hourly by hourly_stats_aggregated_by_advertiser_time_zone per ad for the last seven days (Meta returns no reach, video or off-Meta conversions in hourly rows); and lifetime totals since launch, capped at Meta's 37-month limit. It requests only fixed Meta endpoints, sends bearer credentials server-side, computes app-secret proof when configured, refuses redirects, and follows opaque cursors rather than provider-returned URLs.

Requests are limited to 10 seconds and 2 MB; one spend import accepts at most 200 pages / 20,000 campaign-days and roughly two minutes, and one detailed import at most 300 requests, 60,000 rows, 5,000 objects and four minutes. Every response's X-Business-Use-Case-Usage, X-Ad-Account-Usage and X-FB-Ads-Insights-Throttle header is read; at 90% usage, or when Meta names a regain time, the import stops, keeps the previous snapshot and waits that long (at least 15 minutes) before the next attempt. Throttle codes 4, 17, 613, 80000 and 80004 and Meta's report timeouts are handled the same way. If Meta declines an optional breakdown (device, country, age and gender, or hourly), the rest still imports and the page says which one is missing. Currencies without minor units (KRW, JPY, CLP, COP, CRC, HUF, ISK, IDR, PYG, TWD, VND) are converted with Meta's currency offset, so budgets and spend are stored in the same minor units everywhere. Each import replaces its snapshot atomically after every page validates. A failed import leaves prior spend intact and does not advance coverage. Concurrent imports hold a short lease; credential changes, pausing or deletion invalidate in-flight results. Eight accounts and 200 explicit campaign mappings are allowed per application. Ad objects that no longer exist but still have delivery in the window are kept as Not listed. Reports have an eight-second database budget and show the top 200 campaigns; summary totals include all campaigns. The console fixes the environment to production.

Source management uses revenue.write; reports additionally require revenue.read, analytics.read, and both enabled modules. Console changes require a passkey verified within 30 minutes, CSRF and same-origin validation. Every change, completed or failed import and funnel choice is audited. Management API routes under /apps/{app}/revenue/acquisition use scoped management tokens; public and secret keys cannot read reports or manage connections.

Protocol references: Meta's generated AdAccount API, AdsInsights fields, Meta Business SDK access-token and app-secret setup.