Documentation Integrations

Connect Meta Ads to your Node

Meta tells you how much you spent and how many conversions it believes it produced. Your Node knows how much money actually came in. Connecting the ad account puts both on the same screen — inside your own server, without handing your revenue to the party that grades its own homework.

Time required: 15 minutes of setup, plus a few hours for the 90 days of history to finish arriving.

Start with the tag. Per-ad ROAS only exists if the creative carries utm_content={{ad.id}}. Without that macro, the attributed revenue column comes back zero — and zero looks like bad performance rather than a missing tag. It is Step 1 for a reason: it only applies to clicks that happen after you add it.

What you get

In the Console, under Insights → Meta Ads:

Block What it shows
KPI row Spend, Impressions, Clicks, CTR, CPM, CPC, Conversions and CPA for the period.
Daily delivery The day-by-day series, switchable between spend, impressions, clicks and conversions.
Account ROAS Total revenue your Node recorded in the period against total ad spend, with the breakdown by purchase status.
Entity table Spend by Ad, Ad set or Campaign — and, once you turn on Attributed revenue, the revenue and ROAS of each ad.

What you do not get is a second opinion on delivery. Impressions, clicks and conversions are still the numbers Meta reports. What changes sides is the revenue: it comes from your Node, from the purchases it recorded, not from the conversion the ad platform credited to itself.

None of this passes through Supreme. The Console reads straight from your Node, the Meta token is stored on your server, and the System never sees spend, revenue or events.

This is not a catalog integration. There is no module to install under Integrations → Manage Modules — ad reading is part of the Node from version 4.0.0 onward.


Before you start

Requirement Where to check
Node on version 4.0.0 or newer If Insights is not in the Console sidebar, your Node is not on 4.0.0 yet. Update before continuing.
An active Property Console → Properties. Ad data is stored per Property; with none created, the Console refuses the connection.
Purchases reaching the Node Console → Events. With no purchase recorded there is spend and no revenue — ROAS stays empty, correctly. See Verify your first event.
Access to the Meta ad account You need to be able to mint a token that reads that account.
The ad account's numeric id It is the part after act_. In act_1234567890, the id is 1234567890.

One ad account per Property. Saving again on the same Property replaces the previous connection.


Step 1 — Put the utm_content macro on your creatives

This is the step almost everyone skips, and it decides whether the report is worth reading.

Supreme joins a sale to an ad through the value that arrived in utm_content. Meta fills that value in for you, if you ask, through a dynamic macro.

Where to paste it

In Meta Ads Manager, at the ad level, in the URL parameters field (it sits in the tracking section, next to the ad's destination). Paste:

utm_content={{ad.id}}

If you already use other parameters in that field, join them with &:

utm_source=meta&utm_medium=cpc&utm_campaign={{campaign.name}}&utm_content={{ad.id}}

Meta swaps {{ad.id}} for the ad's numeric id at click time. You never type an id, and you never touch this again when you duplicate an ad.

Attributing by name instead

Supreme also accepts the creative's name:

utm_content={{ad.name}}

If you use this, switch the table's attribution selector from Ad ID to Ad name.

What it costs: names are not unique. Two ads sharing a name are treated as the same creative — both rows show exactly the same attributed revenue, the same sale count and the same ROAS. Only use this mode if your ad naming really identifies one specific piece of media. When in doubt, use the id.

What has to survive until the sale

The join uses the utm_content recorded on the sale, not on the first page visited. Two practical consequences:

  • Preserve the query string across redirects. A link shortener, a domain redirect or an interstitial page that drops the query string erases utm_content before the purchase happens.
  • A sale that reaches the Node only through a webhook carries no utm_content. It counts in total revenue — and therefore in Account ROAS — but it will not appear in that ad's attributed revenue column.

The Console tells you when it is missing

If you turn on Attributed revenue, group by Ad and every row comes back with zero sales, the Console shows the No purchases matched these ads callout with the correct macro ready and a Copy button. That callout means "not tagged", not "no return".


Step 2 — Mint a Meta token with ads_read

The token comes from Meta, not from Supreme. It needs exactly one thing: the ability to read the ad account, which in practice means the ads_read permission on that account.

Two routes, for different purposes:

Route Good for Watch out
A user token from Meta's Graph API Explorer Proving in five minutes that the connection works. It is short-lived. When it expires the connection flips to Token rejected and the sync stops.
A System User token in Meta Business Manager, with access to the ad account Production. This is the right path. It does not die with your personal session.

Meta's screens change often, so memorising a menu path is not worth it: search Meta's own documentation for ads_read and "system user access token". Any token works for Supreme as long as it can read the account — the verification in Step 3 decides that, not you.

Treat the token like a password. It does not go into GTM, into your site, or into a screenshot on a support ticket.


Step 3 — Connect the account in the Console

  1. Open Insights → Meta Ads.
  2. Select Connect ad account.
  3. Choose the Property that will hold this data.
  4. In Ad account id, enter the numeric id. You can paste it with the act_ prefix — the Console strips it.
  5. Paste the Access token.
  6. Leave Attribution window at the default 7d_click,1d_view unless you use a different one on Meta. This value is what the Node reports to Meta when pulling the numbers.
  7. Confirm that Active is checked.
  8. Select Save.

The button goes through Verifying with Meta… before anything is written. The Node verifies the token against the ad account before storing it. A token that cannot read the account is refused on the spot instead of becoming a connection that looks right in the Console and never syncs anything.

That same verification is where the account name, the currency and the timezone on the card come from. You type none of the three — they are read from Meta.

On success the Console confirms: "Ad account connected. The Node started pulling the last 90 days."

If the connection is refused

The Console shows the refusal above the button. When the refusal comes from Meta, the message is passed through exactly as Meta wrote it, with Meta's own code in front — something like [190/-] Invalid OAuth access token. The code is the part that matters.

Situation Cause What to do
The id field complains before any call is made You pasted something non-numeric. Use digits only, with or without the act_ prefix.
Meta returns a token error (190 and neighbours) Wrong, expired or revoked token. Mint a new token.
Meta says the account does not exist or is unreachable The id is fine but the token cannot see that account, or the id does not exist. Confirm the token was minted with access to that account.
The Node says the account returned no currency and timezone The token reads something, but probably lacks ads_read on that account. Mint the token again, including ads_read.
Meta returns a rate limit Nothing wrong on your side. Wait a few minutes and save again.
The Console asks you to create a Property first The Node has no Property, and ad data is stored per Property. Create one under Properties and come back.

The token is never shown again

Once saved, the token does not leave the Node. Not through the API, not through the Console, not through MCP.

What you see on the connection card, under Token, is only a masked preview — the first four characters, an ellipsis, the last four — plus the Stored or Not stored state. If the token is 12 characters or shorter, the preview is just asterisks.

That is by design, not a missing feature. To rotate the token, open Edit and paste a new one; leaving the field empty keeps the one already stored.


Step 4 — Let the 90 days of history arrive

Saving the connection schedules a 90-day backfill. It does not arrive in one shot: the Node pulls it in chunks, so it never blows the server's execution time or Meta's rate limits.

While the Console is open

The moment you save, the Console starts pulling. The connection card shows Backfilling history — N days left, with a progress bar and the window currently being fetched. The status strip at the top reads "Syncing — N days of history left."

You can trigger more chunks at any time with Sync now. Each press chains several chunks in a row, so one press usually finishes the remaining history.

Closing the Console does not lose progress. Each chunk is committed the moment it completes. The sync is idempotent: repeating a window duplicates nothing, it just rewrites the same days.

If you see "Another run holds the sync lock", that is only the cron or another tab working at the same time. It is not an error — the work continues on its own.

Without the Console open: set up a cron

This is the step that decides whether the numbers keep updating on their own. The System triggers cron.php only on Nodes whose license carries the pro access level. On any other Node, nothing calls the cron for you: history only advances while someone has the Console open, and yesterday's spend only lands when someone opens the screen.

The fix is a native hourly cron on your own server, pointing at:

https://YOUR-NODE/cron.php?key=YOUR_AGENT_KEY

The Console builds that URL for you: while history is being pulled, the connection card shows an Hourly cron field with the full URL and a Copy button.

In cPanel, under Cron Jobs, one hourly line:

0 * * * * curl -s "https://YOUR-NODE/cron.php?key=YOUR_AGENT_KEY" > /dev/null

This cron is not only for ads — it is the same schedule that closes sessions, retries failed deliveries and prunes old logs. It is worth setting up even if you never connect an ad account.

When the backfill finishes

The status strip reads "Ad data is up to date." and the card settles into just Synced through and Last sync.

From then on the Node keeps itself current. Once everything is present, it only calls Meta again after the last sync is roughly 20 hours old — and when it does, it re-pulls the last 7 days, not just the new day. That exists because Meta adjusts spend and conversions retroactively: a number from three days ago can still change.


How to read the numbers

Account ROAS and per-ad ROAS answer different questions

The Account ROAS panel divides all the revenue your Node recorded in the period by the total spend. It does not ask where each sale came from. It answers "did this month close in the black?".

The Attr. ROAS column in the table is a different question: within that ad, how much revenue the Node could tie to that ad, against what that ad spent.

The two will diverge, and that is correct. Revenue that came from email, from organic traffic, or from an untagged ad counts in account ROAS and appears in no table row.

Approved revenue sums the statuses approved, paid, completed, complete and authorized. The Revenue by purchase status table shows the full breakdown, so you can see how much was left out and why.

A dash is not a zero

In the attributed columns:

  • a dash means the Node did not cross-check that row;
  • a zero means it cross-checked and found no sale.

The join only runs per Ad. Grouped by Ad set or Campaign the Node returns no attribution — a sale carries the ad it came from, not the ad set — and the columns show dashes.

Timezone: why the comparison can shift by a day

If the ad account is in one timezone and the Node in another, a spend day and a revenue day do not cover the same 24 hours. Spend is totalled on the ad account's calendar; revenue on the Node's.

In the middle of a long period this all but cancels out. At the edges — the first and last day — the comparison can move by up to a day.

The Console flags the difference when it detects it, both on the connection card and in the ROAS panel. It cannot be corrected: both sides hand over daily totals, each on its own calendar, and there is not enough information to re-slice that by hour. Read the ratio as an approximation and prefer periods of 7 days or more.

Currency

Spend arrives in the ad account's currency; revenue in the Node's. When the two differ, the Console says the ratio mixes two currencies and does not convert. There is no exchange-rate conversion built in.

Why Today is usually empty

The sync runs through yesterday, on the ad account's calendar. Current-day numbers are still forming on Meta's side and shift throughout the day. Use Yesterday, 7d or a closed period for any reading you intend to compare.

Dashes in the KPI row

CPM, CPC, CPA and ROAS show a dash when the denominator is zero — no impressions, no CPM; no reported conversions, no CPA. A dash there means "nothing to divide by", not "zero".


Maintenance

Rotating the token

Open Edit on the connection, paste the new token and save. Leaving Access token empty keeps the stored one — useful when you only want to change the attribution window or pause the connection.

This is what you do when the card reads Token rejected: the token expired or was revoked on Meta's side, and the Node has no way to renew it on its own.

Changing the ad account

Changing the Ad account id on an existing connection resets the sync watermarks and starts a fresh 90-day backfill. The Console warns you first. It is not a free way to fix a mistyped digit.

Pausing

Uncheck Active and save. The connection stops syncing and stays listed as Paused, with all history intact. It is the right way to stop temporarily.

Disconnecting

Select the trash icon on the connection and confirm with Confirm disconnect.

Disconnecting keeps the history already synced. Reconnecting the same account later does not start from zero — that is exactly why keeping it is the default.

Deleting the history is an explicit choice: check Also delete the synced insight history for this account before confirming, and the button changes to Disconnect and delete history. That removal cannot be undone.

In neither case does Supreme touch a campaign, an audience or any configuration on Meta's side. Disconnecting here only stops the reading.


Troubleshooting

The card shows Token rejected

The token expired, was revoked, or lost access to the account. Mint a new one with ads_read, open Edit and paste it. The sync resumes where it stopped. If you used a Graph API Explorer token, this will happen again — replace it with a System User token.

The card shows Rate limited

Meta is throttling calls for that account. The sync stopped exactly where it was and nothing was lost. Wait a few minutes and select Sync now. If it happens daily, several tools are probably reading the same account at once.

The table is empty but the account shows Connected

Widen the period. The sync runs through yesterday, and an account connected a few minutes ago may have no day stored yet. If 30d is still empty, select Sync now and watch the status strip.

Every attributed revenue value is zero

That is tagging, not performance. Check, in this order:

  1. Does the creative carry utm_content={{ad.id}} in the ad's URL parameters field?
  2. Is the table selector on Ad ID — or on Ad name, if you used {{ad.name}}?
  3. Did the clicks behind those sales happen after you added the macro? It is not retroactive.
  4. Does the query string survive every redirect through to the page where the purchase is recorded?
  5. Do those sales arrive from the browser, or only through a webhook? A webhook-only sale carries no utm_content.

In Events, open a recent purchase and check whether it has utm_content filled in. That is the most direct test: if the event does not carry it, there is nothing to join.

History does not advance when nobody has the Console open

That is the cron case. Set up the hourly schedule from Step 4. To confirm it works, call the URL once in a browser: the response is JSON with the result of each task, including the ad sync.

ROAS looks far too high or too low

Before suspecting the number, check three things:

  • Currency. Are spend and revenue in the same currency? The Console says so when they are not.
  • Timezone. Is the timezone-mismatch warning showing? Widen the period.
  • Status. Open Revenue by purchase status. A pile of purchases in a non-approved status explains a low ROAS with no measurement error at all.

What each check proves

  • A number in the panel proves the Node read that value from Meta and stored it. It does not prove Meta delivered the ad to the right person.
  • An attributed revenue figure proves the Node found a purchase of yours carrying that ad id. It is your measurement, from your data — it is not Meta Ads Manager's attribution, and the two numbers do not have to agree.
  • A Synced through date moving forward proves the cron or the Console is running. It does not prove the token will still be valid tomorrow.

To expose these same numbers to your own AI agent, see Connect an MCP client to your Node.