Cross-Platform Ad Reporting: Meta + Google Ads in One API
How to build unified ad reporting across Meta and Google Ads with a single API. Covers data normalization, common metrics, and practical code examples.
The Cross-Platform Reporting Problem
Most businesses advertise on more than one platform. A typical mid-size e-commerce company might run prospecting campaigns on Meta, search campaigns on Google, and video campaigns on TikTok. Each platform has its own API, its own data format, its own authentication system, and its own definition of basic metrics.
When someone asks "how much did we spend on advertising last month?" the answer requires pulling data from multiple sources, normalizing it into a common format, and aggregating it. This sounds simple. It is not.
Here is what you are actually dealing with:
Meta Ads API
- Budgets are strings in cents:
"5000"means $50.00 - Conversions are nested in an
actionsarray keyed byaction_type - Metrics use string values:
"impressions": "142893" - Date ranges use presets like
last_30dor explicittime_rangeobjects - Pagination uses cursor-based paging
Google Ads API
- Uses a custom query language called GAQL (Google Ads Query Language)
- Metrics are in micros:
1000000means $1.00 - Campaign types use internal codes like
SEARCH,DISPLAY,VIDEO - Authentication requires OAuth 2.0 with refresh tokens
- Responses use protobuf-style nested objects
The Normalization Challenge
Consider a simple metric: cost per conversion. To calculate it from raw platform data:
Meta:
spend (string, cents) / actions.find(a => a.action_type === "purchase").value (string)
= parseFloat("48723") / 100 / parseFloat("87")
= $5.60
Google:
metrics.cost_micros / 1000000 / metrics.conversions
= 4872300 / 1000000 / 87
= $5.60
Same number, completely different paths to get there. Now multiply this by every metric, every campaign, every platform, and you see why cross-platform reporting is painful.
Building Unified Reports with Xylo
Xylo normalizes data from every supported platform into a consistent format. A campaign from Meta and a campaign from Google return the same shape:
interface Campaign {
id: string;
platform: "meta" | "google" | "tiktok";
name: string;
status: "active" | "paused" | "archived" | "deleted";
objective: string;
daily_budget: number; // Always dollars, always a number
lifetime_budget: number | null;
start_date: string; // Always ISO 8601
end_date: string | null;
insights: {
impressions: number; // Always a number
clicks: number;
spend: number; // Always dollars
cpc: number;
cpm: number;
ctr: number; // Always a percentage
conversions: number;
cost_per_conversion: number;
};
}
No cents-to-dollars conversion. No micros-to-dollars conversion. No array-searching for action types. Every field is a number (not a string), and every monetary value is in dollars.
Practical Example: Weekly Performance Dashboard
Here is a complete example of pulling cross-platform data for a weekly dashboard:
const XYLO_API = "https://api.xyloapi.dev/v1";
const headers = {
"x-api-key": process.env.XYLO_API_KEY!,
};
interface PlatformAccount {
platform: string;
account_id: string;
}
const accounts: PlatformAccount[] = [
{ platform: "meta", account_id: "act_123456789" },
{ platform: "google", account_id: "customers/1234567890" },
];
async function fetchCampaigns(account: PlatformAccount) {
const response = await fetch(
`${XYLO_API}/campaigns?date_preset=last_7d`,
{
headers: {
...headers,
"x-ad-account": account.account_id,
},
}
);
return response.json();
}
// Fetch all platforms in parallel
const results = await Promise.all(accounts.map(fetchCampaigns));
// Aggregate metrics across platforms
const totals = {
spend: 0,
impressions: 0,
clicks: 0,
conversions: 0,
};
const allCampaigns = results.flatMap((r) => r.data);
for (const campaign of allCampaigns) {
totals.spend += campaign.insights.spend;
totals.impressions += campaign.insights.impressions;
totals.clicks += campaign.insights.clicks;
totals.conversions += campaign.insights.conversions;
}
console.log(`Total spend: $${totals.spend.toFixed(2)}`);
console.log(`Total impressions: ${totals.impressions.toLocaleString()}`);
console.log(`Overall CPC: $${(totals.spend / totals.clicks).toFixed(2)}`);
console.log(`Overall CPA: $${(totals.spend / totals.conversions).toFixed(2)}`);
Because Xylo normalizes the data, you can aggregate across platforms without any platform-specific logic. The code above works the same whether you have 2 platforms or 5.
Platform-Specific Breakdowns
While aggregation is useful, you often need to compare platforms side by side. The platform field on each campaign makes this straightforward:
// Group by platform
const byPlatform = allCampaigns.reduce((acc, campaign) => {
const platform = campaign.platform;
if (!acc[platform]) {
acc[platform] = { spend: 0, conversions: 0, campaigns: 0 };
}
acc[platform].spend += campaign.insights.spend;
acc[platform].conversions += campaign.insights.conversions;
acc[platform].campaigns += 1;
return acc;
}, {} as Record<string, { spend: number; conversions: number; campaigns: number }>);
// Output comparison
for (const [platform, data] of Object.entries(byPlatform)) {
const cpa = data.conversions > 0
? (data.spend / data.conversions).toFixed(2)
: "N/A";
console.log(
`${platform}: $${data.spend.toFixed(2)} across ${data.campaigns} campaigns, CPA: $${cpa}`
);
}
Example output:
meta: $3,247.50 across 12 campaigns, CPA: $8.42
google: $2,891.30 across 8 campaigns, CPA: $12.67
Common Metrics and Definitions
One of the trickiest parts of cross-platform reporting is that platforms define metrics differently. Here is how Xylo normalizes the most common ones:
| Metric | Meta Definition | Google Definition | Xylo Output |
|---|---|---|---|
| Impressions | Times ad was on screen | Times ad was shown | impressions: number |
| Clicks | All clicks (link + other) | Clicks on ad | clicks: number |
| CTR | clicks / impressions * 100 | clicks / impressions * 100 | ctr: number (percentage) |
| Spend | Amount spent (string, cents) | Cost (micros) | spend: number (dollars) |
| CPC | spend / clicks | cost / clicks | cpc: number (dollars) |
| CPM | spend / impressions * 1000 | cost / impressions * 1000 | cpm: number (dollars) |
| Conversions | actions[type=purchase] | conversions | conversions: number |
| CPA | spend / purchase actions | cost / conversions | cost_per_conversion: number |
An important note: "clicks" means slightly different things on each platform. Meta includes all clicks (link clicks, post engagement, etc.) while Google counts clicks on the ad itself. Xylo uses the platform's primary click metric in each case and documents the distinction in the API reference.
Handling Time Zones and Date Ranges
Ad platforms report data in different time zones:
- Meta uses the ad account's time zone
- Google uses the account's time zone (set at the MCC level)
When aggregating cross-platform data, make sure your accounts use the same time zone, or account for the offset. Xylo returns dates in ISO 8601 format with the account's time zone, so you can handle conversion in your application layer.
For consistent reporting, use the same date_preset across all requests:
const datePreset = "last_7d"; // Same period for all platforms
const [metaData, googleData] = await Promise.all([
fetch(`${XYLO_API}/campaigns?date_preset=${datePreset}`, {
headers: { ...headers, "x-ad-account": metaAccount },
}),
fetch(`${XYLO_API}/campaigns?date_preset=${datePreset}`, {
headers: { ...headers, "x-ad-account": googleAccount },
}),
]);
Building Automated Reports
A common pattern is running a daily or weekly report that pulls data from all platforms and sends a summary. Here is a skeleton for a daily email report:
async function generateDailyReport() {
const accounts = await getConnectedAccounts(); // from your database
const results = await Promise.all(
accounts.map((account) =>
fetch(`${XYLO_API}/campaigns?date_preset=yesterday`, {
headers: {
"x-api-key": process.env.XYLO_API_KEY!,
"x-ad-account": account.id,
},
}).then((r) => r.json())
)
);
const campaigns = results.flatMap((r) => r.data);
// Find winners and losers
const sorted = campaigns
.filter((c) => c.insights.spend > 0)
.sort((a, b) => a.insights.cost_per_conversion - b.insights.cost_per_conversion);
const topPerformers = sorted.slice(0, 5);
const bottomPerformers = sorted.slice(-5).reverse();
return {
date: new Date().toISOString().split("T")[0],
total_spend: campaigns.reduce((sum, c) => sum + c.insights.spend, 0),
total_conversions: campaigns.reduce((sum, c) => sum + c.insights.conversions, 0),
top_performers: topPerformers.map((c) => ({
name: c.name,
platform: c.platform,
spend: c.insights.spend,
conversions: c.insights.conversions,
cpa: c.insights.cost_per_conversion,
})),
bottom_performers: bottomPerformers.map((c) => ({
name: c.name,
platform: c.platform,
spend: c.insights.spend,
conversions: c.insights.conversions,
cpa: c.insights.cost_per_conversion,
})),
};
}
Caching and Rate Limits
When pulling data from multiple platforms, rate limits become a real concern. Each platform has its own limits, and hitting them causes delays and failures.
Xylo handles this transparently:
- Built-in caching reduces the number of calls to upstream APIs. If you request the same data within the cache TTL (5 minutes for campaign lists, 15 minutes for insights), you get a cached response instantly.
- Automatic retry with exponential backoff handles transient rate limit errors from upstream platforms.
- Cache metadata in every response tells you whether the data was cached and how old it is:
{
"meta": {
"cached": true,
"cache_age_seconds": 127,
"meta_api_version": "v22.0"
}
}
This means your cross-platform dashboard can poll for updates without worrying about rate limit management.
Getting Started
Building cross-platform ad reporting with Xylo:
- Create a Xylo account and connect your ad accounts from each platform.
- Generate an API key from the dashboard.
- Use the examples above to pull normalized data from all platforms.
- Aggregate, compare, and report -- the data format is already consistent.
For AI-powered reporting, check out our guide on building AI agents for ads. The MCP server can generate cross-platform reports through natural conversation.
The API documentation covers all available endpoints, parameters, and response formats for each platform.
Ready to simplify your ads API integration?
Get started with Xylo in minutes. One API key for every ad platform.