VoidhashDocs
DocumentationAPI reference
Dashboard
Get started
Introduction
Quickstart
Setup
Setup overview
Create a project
Products and perks
Paywall locations
CLI
Paywall components
Components
Develop a component
Editor panels
Component definition
Publishing
Installing and capabilities
SDKs
Swift
Kotlin
React Native
Web
Store setup
Connect the App Store
Connect Google Play
Connect Stripe
Record payments from other sources
API libraries
Node.js
PHP
Rust
Go
Resources
MCP server
Ad measurement
Meta ads
Migrate in observer mode
Delete a person's data

Meta ads

Connect a Meta ad account to compare campaign spend with the trials, payers and revenue it brings in.

Connect a Meta (Facebook and Instagram) ad account to your project and Voidhash keeps a read-only copy of its campaigns and daily spend. It joins that spend with the people each campaign, ad set and ad brought in, and with what those people went on to do, so one report shows cost per trial, cost per payer and the return on ad spend measured on real revenue.

Voidhash only reads from Meta. It never changes a campaign, budget or ad.

Before you start

Voidhash attributes people to an ad from the ad's landing page URL. Add the campaign, ad set and ad IDs to every ad's URL parameters and install the web SDK on the landing page, as described in Page views and attribution. Without the IDs, spend still syncs, but no people are matched to it.

You also need:

  • Admin access to the business in Meta Business Manager that owns the ad account.
  • An app owned by that business. Any app type works; it is only used to issue the token.
  • A Voidhash user or secret key with the project:ads:manage permission to connect the account.

Create a system user token

A system user is a non-human user of your business. Its token keeps working when people leave your team and does not need Meta's app review.

Add a system user

In Business Settings, open Users → System users and select Add. Name it, for example Voidhash reporting, and give it the Employee role.

Give it the ad account

With the system user selected, choose Assign assets, then Ad accounts. Select the ad account and turn on View performance. Voidhash needs no other access.

Generate the token

Select Generate new token, choose your business's app, and pick the ads_read permission. For the expiration, choose Never so the sync keeps running. Copy the token; Meta shows it once.

Find the ad account ID

In Ads Manager, open the account menu. The ad account ID is the number shown under the account name. You can enter it with or without the act_ prefix.

Treat the token like a password. Voidhash stores it encrypted and never shows it again, in the dashboard or through the API.

Connect the account

In the dashboard, open Settings → Ad platforms and select Connect next to Meta. Enter the token and choose the ad accounts to read. Voidhash checks that the token grants ads_read and can read the account, then records the account's name, currency and time zone. If the check fails, the message says what to fix.

Through the API, send the same values to POST /api/v1/projects/{projectId}/ads/accounts, with the ad account ID as externalId:

curl https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/ads/accounts \
  -H "Authorization: Bearer $VOIDHASH_SECRET_KEY" \
  -H "content-type: application/json" \
  -d '{
    "platform": "meta",
    "externalId": "act_1234567890",
    "accessToken": "EAAB...",
    "backfillDays": 90
  }'

Connecting an account that is already connected replaces its token. Use this when you rotate the token or after a sync stopped. A project can connect several ad accounts.

What is synced, and how often

The first sync starts as soon as the account is connected. After it, Voidhash syncs every account at the start of every hour.

  • Account health: the account's status in Meta (active, disabled, unpaid balance, grace period, under review or closed) with Meta's reason, its spending limit and how much of it is spent, and the business portfolio that owns it.
  • Campaigns, ad sets and ads: IDs, names, status, budget, bid strategy, objective, the event each ad set optimizes for, start and end, where each ad sends people, and when each was created and last updated. Archived and deleted objects are included; an object Meta stops listing is marked removed and keeps its last known name.
  • Creatives: format (image, video or carousel), thumbnail, video length, primary text, headline, call to action, landing URL and URL parameters, and which ads use each one.
  • Daily delivery per ad: spend, impressions, clicks, link clicks, results, video plays of at least three seconds and ThruPlays, and Meta's reported purchases with their value, app installs, leads, landing page views, registrations, subscriptions and trial starts, for ads in every status, including paused, archived and deleted ones. Each day is dated in the ad account's time zone, and amounts are in its currency. Conversions are counted with the ad account's attribution setting, the same numbers Ads Manager shows, and the setting is stored with each day. Meta reports one conversion under several overlapping action types, so each kind counts the first type present, starting with the cross-channel total (such as omni_purchase), instead of adding them up.
  • Breakdowns: the same daily delivery split by platform (Facebook, Instagram, Audience Network, Messenger), placement, country, device, and age and gender, for the last 28 days.
  • Daily account totals: the whole account's spend, impressions and clicks per day. They include spend Meta no longer reports ad by ad, such as on some deleted ads, so totals stay complete.

Reach and frequency count people, so they don't add up across ads or days. Voidhash reads them from Meta for exactly the campaigns, ad sets, ads and days you look at, and never adds them together; a total that would need adding them shows no reach.

The first sync pulls the last 90 days, or the number of days you set with backfillDays, up to three years. Meta keeps adjusting delivery and attributed conversions for up to 28 days, so every six hours a sync pulls the last 28 days and the breakdowns again and replaces what it stored for the ads Meta reports. Days stored for an ad Meta no longer reports are kept. Connecting the account again with a larger backfillDays pulls the longer history on the next sync.

The hourly syncs in between read the account and every campaign, ad set and ad, and pull daily delivery for today and yesterday. When the account has spent nothing since the previous sync, they skip delivery; conversions Meta attributes to earlier days later arrive with the next six-hourly sync.

Each account under Settings → Ad platforms → Meta shows when it last synced, the last error, and when the next sync runs.

  • A sync that fails because Meta is unavailable or rate-limits the request is retried an hour later, or later still when Meta asks for a longer pause. The status shows Sync error until a sync succeeds.
  • When the token is revoked or expires, or loses access to the ad account, syncing stops and the error says why. Generate a new token, select Reconnect and enter it to resume; the connected accounts are selected, and their token is replaced.

Statuses

Each campaign, ad set and ad has two statuses and the reasons behind them:

  • Configured is what you set in Ads Manager: on, paused, or removed (archived or deleted).
  • Delivery says whether it runs: delivering, learning (with its progress, such as 31 of the 50 optimization events Meta needs in seven days), learning limited, in review, rejected, paused, off because a parent campaign or ad set is off, scheduled, ended, not delivering, or stopped by an account issue such as missing payment information.

Reasons explain anything less than delivering: the policy Meta rejected an ad under, with Meta's explanation; issues Meta reports on the object; learning that can't finish; or, for a campaign or ad set, children that don't deliver or are rejected. An ad set whose ads all wait for review or their start date shows as in review or scheduled, not as not delivering. A campaign is shown as learning or learning limited when most of its delivering ad sets are. Voidhash only reports these states. Change them in Ads Manager; every object has a link that opens it there.

After every sync, Voidhash also lists the account's issues: rejected ads, learning-limited ad sets, objects that are on but not delivering, account problems, a spending limit that is 80% or more used, and a sync that failed.

Destinations

Each ad is marked by where it sends people:

  • App store: app promotion campaigns, and ad sets that promote an app.
  • Website: ads that link to a web page.
  • Web-to-app: website ads that link to a page that hands people over to your app. List the start of those pages' addresses, such as example.app/start, as your project's web-to-app landing pages. An ad whose link is that page, or a page under it, is web-to-app. Changing the list applies to synced ads right away.

Ad sets and campaigns take their ads' destination when all of them share one, and are marked mixed when their ads go to different ones.

Voidhash also checks that every active website ad's link carries campaign_id (or utm_id), adset_id and ad_id, either in its URL or in its URL parameters, and reports the ads, and the share of spend, that miss them, in the URL parameters tab of Settings → Ad platforms → Meta.

To disconnect an account, open its menu in Settings → Ad platforms → Meta and select Disconnect. Disconnecting an account deletes everything synced from it. Connecting, reconnecting and disconnecting are recorded in the project's audit log, without the token.

Campaign performance

The Ads → Campaigns page, the performance report and the ads_performance_report MCP tool report one row per campaign, ad group (Meta's ad set) or ad for a range of days:

curl https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/ads/reports/performance \
  -H "Authorization: Bearer $VOIDHASH_SECRET_KEY" \
  -H "content-type: application/json" \
  -d '{ "from": "2026-09-01", "to": "2026-09-30", "level": "campaign" }'

level is campaign, ad_group or ad. You can narrow the rows with campaignIds, adGroupIds, delivery, destination or search, split each row with a breakdown (placement, country, age, gender, device, or surface for Voidhash's numbers), and sort them. Rows come a page at a time: pass the previous page's pageInfo.endCursor as cursor. totals cover every matching row, not only the page.

Each row keeps two sets of numbers apart: what Meta reports, in reported, and what Voidhash measured from the people the ad brought in, in measured. Money is an integer number of minor units of the report's currency, and rates are decimals, such as 0.0125 for 1.25%.

reportedMeaning
spend, impressionsMeta's delivery for the days in the range.
linkClicks, ctrLink clicks, and linkClicks / impressions.
resultsWhat Meta counted as results, by type, such as landing_page_view or app_install. Results of different types are never added up.
purchaseValue, platformRoasThe purchase value Meta attributes to the ad, and that value divided by spend.
reach, frequencyOnly when you ask with "include": ["reach"], which makes the report slower. Never added up, so totals have none.
measuredMeaning
visits, installsWeb visits that arrived from the ad, and acquired people your app reported. null where there is no such step.
signupsPeople who signed up and whose first visit came from the ad.
trials, payersAcquired people who started a trial, and those who paid anything.
keyConversionsAcquired people who reached your key conversion, named in keyConversion.
grossRevenue, refunds, netRevenueWhat acquired people paid, what was refunded, and the difference.
costPerSignup, costPerPayer, costPerKeyConversionspend divided by signups, payers or keyConversions.
roasTrue return on ad spend: netRevenue / spend.

A ratio is null when what it divides by is zero. People are attributed by their first touch, as in the acquisition report (see ad measurement for the definitions), so roas counts each person's revenue once, on the ad that first brought them in. platformRoas uses Meta's own attribution and can count the same purchase for several ads; the two often differ.

Reading the report requires project:ads:read. Voidhash's numbers also need project:analytics:read: without it the report still answers, with hasMeasuredAccess set to false and every measured set to null. Account delivery that no ad row accounts for, usually on deleted ads, is reported as unallocated, so the totals and unallocated together match the accounts' spend. It is null when filters narrow the rows.

By default money is reported in the connected accounts' currency and days follow their time zone, so spend and outcomes line up with Ads Manager; accounts that differ default to USD and UTC. You can ask for another currency; spend is then converted at each day's exchange rate. When you ask for another timezone, people and revenue follow it but spend stays dated in the account's time zone. The report's warnings say when either happened, and when an account has not finished syncing.

Revenue comes from production purchases unless you set environment. Recent days are incomplete: spend for today is still accruing, and people acquired recently have had less time to convert. provisionalFrom is the first day whose numbers can still change.

What changed

Each sync also records what changed since the previous one, so you can see when a decision was made and what the numbers did around it:

  • Decisions: a campaign, ad set or ad was created, turned on, paused or removed, or its budget or bid strategy changed, with the values before and after. Other edits, such as names, targeting or creatives, are not recorded. A change is dated by the time Meta says the object was created or last edited, when that falls between two syncs; otherwise by the sync that found it.
  • Delivery: what each ad spent, showed, got clicked and converted between two syncs, normally the hour before.
  • Revisions: Meta's corrections to days that had already ended, mostly conversions it attributed late. They are neither new delivery nor the effect of a decision.

The changes report and the ads_changes_report MCP tool return them for a period of up to 7 days, added up per campaign, ad set or ad:

curl https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/ads/reports/changes \
  -H "Authorization: Bearer $VOIDHASH_SECRET_KEY" \
  -H "content-type: application/json" \
  -d '{ "since": "2026-10-28T00:00:00Z", "level": "campaign" }'

The numbers are Meta's, in each ad account's currency (minor units) and time zone. History starts when Voidhash began keeping it for an account, shown as historySince; the first sync after that stores where each day stood, and only later changes count. Disconnecting an account deletes its history.

PreviousAd measurementNextMigrate in observer mode

On this page

Before you startCreate a system user tokenAdd a system userGive it the ad accountGenerate the tokenFind the ad account IDConnect the accountWhat is synced, and how oftenStatusesDestinationsCampaign performanceWhat changed