Omnia docs

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:

MetricWhat it measuresEndpoint suffix
Share of voiceHow often a brand is mentioned relative to competitors/share-of-voice/aggregates
VisibilityHow prominently a brand appears in AI responses/visibility/aggregates
CitationsWhich sources AI engines cite when mentioning brands/citations/aggregates
SentimentHow 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 fetch API)
  • 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.ts

Next 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.

On this page