Export performance data
Export daily performance data from the Omnia API into flat JSON files that you can load into Looker Studio, BigQuery, Tableau, or any BI tool.
How data is organized
Omnia tracks how AI engines mention and recommend brands. The data is organized in a three-level hierarchy:
-
Brand is the top level. It represents your company or product (e.g. "Acme Corp" / acme.com). Brand-level aggregates show your overall performance across all topics.
-
Topics sit under a brand. Each topic is a subject area you want to monitor (e.g. "project management software", "best CRM tools"). Topic-level aggregates show how brands perform within that specific subject.
-
Prompts sit under a topic. Each prompt is a specific question that gets asked to AI engines (e.g. "What is the best project management tool for remote teams?"). Prompt-level aggregates show brand performance for that exact question.
At each level, four metrics are available:
| Metric | What it measures | Endpoint suffix |
|---|---|---|
| Share of voice | How often a brand is mentioned relative to competitors | /share-of-voice/aggregates |
| Visibility | How prominently a brand appears in AI responses | /visibility/aggregates |
| Citations | Which sources AI engines cite when mentioning brands | /citations/aggregates |
| Sentiment | How positively or negatively AI engines describe brands | /sentiment/aggregates |
That gives you 3 levels x 4 metrics = 12 aggregate endpoints total. Share of voice and visibility return the same shape (brand name, mention count, rank). Citations return a different shape (URL, title, citation count). Sentiment returns a flat list of brand-feature pairs with endorsedMentions/underminedMentions/neutralMentions counts. The code below uses share of voice as the example.
Prerequisites
- An Omnia API key (generate one from your API access page)
- Node.js 18+ (uses the built-in
fetchAPI) - tsx to run TypeScript directly:
npm install -g tsx
Store your API key in an environment variable. Do not hardcode it or commit it to version control.
export OMNIA_API_KEY="your-api-key-here"Step 1: API client
Create a file called export-sov.ts. Start with two utility functions: apiFetch handles
authentication and retries on 429, and buildUrl constructs an endpoint URL with query params:
import fs from "node:fs";
const API_BASE = "https://app.useomnia.com/api/v1";
const API_KEY = process.env.OMNIA_API_KEY;
const MAX_RETRIES = 3;
if (!API_KEY) {
throw new Error("Set the OMNIA_API_KEY environment variable before running this script.");
}
function buildUrl(endpoint: string, params: Record<string, string> = {}): URL {
const url = new URL(API_BASE + endpoint);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
return url;
}
async function apiFetch(url: string | URL, retries = 0): Promise<any> {
const response = await fetch(url, {
headers: { Authorization: `Bearer \${API_KEY}` },
});
if (response.status === 429) {
if (retries >= MAX_RETRIES) {
throw new Error("Rate limit exceeded after " + MAX_RETRIES + " retries");
}
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "5", 10);
await new Promise((resolve) => setTimeout(resolve, retryAfter \* 1000));
return apiFetch(url, retries + 1);
}
if (!response.ok) {
let description = response.statusText;
try {
const body = await response.json();
description = body.error.description;
} catch {}
throw new Error(`API \${response.status}: \${description}`);
}
return response.json();
}Step 2: Pagination helper
Aggregate endpoints are paginated. Every paginated response includes a links object with
a next URL. Follow it until there are no more pages:
async function fetchAllPages(
endpoint: string,
dataKey: string,
params: Record<string, string> = {}
) {
const allItems: Record<string, unknown>[] = [];
let nextUrl: string | URL | undefined = buildUrl(endpoint, { ...params, pageSize: "100" });
while (nextUrl) {
const page = await apiFetch(nextUrl);
allItems.push(...(page.data[dataKey] ?? []));
nextUrl = page.links?.next;
}
return allItems;
}Step 3: Find your brand and export
The rest of the script goes inside an async main() function. Start by calling GET /brands
to find the brand you want to export:
async function main() {
const brands = await fetchAllPages("/brands", "brands");
const brand = brands.find((b) => b.name === "Your Brand Name");
if (!brand) {
throw new Error("Brand not found. Available: " + brands.map((b) => b.name).join(", "));
}
const brandId = brand.id as string;
console.log(`Exporting share of voice for \${brand.name} (\${brandId})`);Replace "Your Brand Name" with the name of your brand as it appears in Omnia.
Step 4: Loop over a date range and write to a file
Aggregate endpoints accept startDate and endDate. To get daily granularity, set both to the
same date and loop one day at a time:
const startDate = new Date("2025-06-01T00:00:00Z");
const endDate = new Date("2025-06-07T00:00:00Z");
const rows: Record<string, unknown>[] = [];
for (const d = new Date(startDate); d <= endDate; d.setUTCDate(d.getUTCDate() + 1)) {
const date = d.toISOString().slice(0, 10);
console.log(`Fetching \${date}...`);
const aggregates = await fetchAllPages(
`/brands/\${brandId}/share-of-voice/aggregates`,
"aggregates",
{ startDate: date, endDate: date }
);
for (const item of aggregates) {
rows.push({ date, ...item });
}
}
fs.writeFileSync("share-of-voice.json", JSON.stringify(rows, null, 2));
console.log(`Wrote \${rows.length} rows to share-of-voice.json`);
}
main();Running the script
All the code blocks above go into the same file (export-sov.ts), in order. Then run it:
tsx export-sov.tsNext steps
The example above covers brand-level share of voice. To export at the topic or prompt level, replace the endpoint path:
/brands/{id}/share-of-voice/aggregates(brand level)/topics/{id}/share-of-voice/aggregates(topic level)/prompts/{id}/share-of-voice/aggregates(prompt level)
Swap share-of-voice for visibility, citations, or sentiment to get the other metrics. Note that
citations and sentiment return different response shapes (see the endpoint reference above for details).
To discover all topics and prompts for a brand, add this inside your main() function:
const topics = await fetchAllPages(`/brands/\${brandId}/topics`, "topics");
for (const topic of topics) {
const prompts = await fetchAllPages(`/topics/\${topic.id}/prompts`, "prompts");
console.log(`\${topic.name}: \${prompts.length} prompts`);
}From there, loop over each topic and prompt the same way the brand example loops over dates.
For a production-ready version that exports all metrics at all levels with concurrency, rate limit handling, and error recovery, see the full export script on GitHub.