Skip to content
Open
294 changes: 274 additions & 20 deletions docs/index.html

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion docs/llmo-brandalf-apis/brand-presence-apis-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Single entry point for all org-scoped Brand Presence HTTP APIs backed by mystica

Parameters are typically supplied as **query string** fields (merged into request `data` by the API gateway). Aliases such as `start_date` / `startDate` are noted per endpoint.

Deep-dive docs: [filter-dimensions](filter-dimensions-api.md), [weeks](brand-presence-weeks-api.md), [sentiment-overview](sentiment-overview-api.md), [market-tracking-trends](market-tracking-trends-api.md), [topics & prompts](topics-api.md), [search](search-api.md), [topic detail](topic-detail-api.md), [prompt detail](prompt-detail-api.md), [sentiment-movers](sentiment-movers-api.md), [share-of-voice](share-of-voice-api.md), [stats](brand-presence-stats-api.md).
Deep-dive docs: [filter-dimensions](filter-dimensions-api.md), [weeks](brand-presence-weeks-api.md), [sentiment-overview](sentiment-overview-api.md), [market-tracking-trends](market-tracking-trends-api.md), [topics & prompts](topics-api.md), [search](search-api.md), [topic detail](topic-detail-api.md), [prompt detail](prompt-detail-api.md), [sentiment-movers](sentiment-movers-api.md), [share-of-voice](share-of-voice-api.md), [stats](brand-presence-stats-api.md), [brand-vs-competitors](brand-vs-competitors-api.md).

---

Expand All @@ -31,6 +31,7 @@ Each **Query parameters** cell lists one parameter per line as `name : sample (o
| 10 | GET | `/org/:spaceCatId/brands/{all\|:brandId}/brand-presence/sentiment-movers` | Top/bottom prompts by sentiment change (RPC `rpc_sentiment_movers`). | <ul><li><code>type</code> : <code>top</code> or <code>bottom</code> (optional)</li><li><code>startDate</code> : <code>2026-02-09</code> (optional)</li><li><code>endDate</code> : <code>2026-03-09</code> (optional)</li><li><code>model</code> / <code>platform</code> : <code>google-ai-mode</code> (optional)</li><li><code>siteId</code> : site UUID (optional)</li><li><code>categoryId</code> : category UUID (optional)</li><li><code>regionCode</code> / <code>region</code> : <code>US</code> (optional)</li><li><code>origin</code> : <code>human</code> (optional)</li><li><code>topicIds</code> : comma-separated UUIDs (optional)</li></ul> | [§10](#10-sentiment-movers) | [sentiment-movers-api.md](sentiment-movers-api.md) |
| 11 | GET | `/org/:spaceCatId/brands/{all\|:brandId}/brand-presence/share-of-voice` | Per-topic share of voice, competitors, rankings (`rpc_share_of_voice`). | <ul><li><code>startDate</code> : <code>2025-09-27</code> (optional)</li><li><code>endDate</code> : <code>2025-09-30</code> (optional)</li><li><code>model</code> : <code>gemini</code> (optional)</li><li><code>siteId</code> : site UUID (optional)</li><li><code>categoryId</code> : <code>0178a3f0-1234-7000-8000-000000000099</code> (optional)</li><li><code>topicIds</code> : comma-separated UUIDs (optional)</li><li><code>regionCode</code> / <code>region</code> : <code>US</code> (optional)</li><li><code>origin</code> : <code>ai</code> (optional)</li><li><code>maxCompetitors</code> / <code>max_competitors</code> : <code>5</code> (optional)</li></ul> | [§11](#11-share-of-voice) | [share-of-voice-api.md](share-of-voice-api.md) |
| 12 | GET | `/org/:spaceCatId/brands/{all\|:brandId}/brand-presence/stats` | Org/brand execution totals and averages via `rpc_brand_presence_stats`; optional weekly slices. | <ul><li><code>startDate</code> : <code>2025-01-01</code> (optional)</li><li><code>endDate</code> : <code>2025-01-31</code> (optional)</li><li><code>model</code> / <code>platform</code> : <code>gemini</code> (optional)</li><li><code>showTrends</code> / <code>show_trends</code> : <code>true</code> (optional)</li><li><code>siteId</code> : site UUID (optional)</li><li><code>categoryId</code> : category UUID (optional)</li><li><code>topicIds</code> : comma-separated UUIDs (optional)</li><li><code>regionCode</code> / <code>region</code> : <code>US</code> (optional)</li><li><code>origin</code> : <code>ai</code> (optional)</li></ul> | [§12](#12-stats) | [brand-presence-stats-api.md](brand-presence-stats-api.md) |
| 13 | GET | `/org/:spaceCatId/brands/{all\|:brandId}/brand-presence/brand-vs-competitors` | Aggregated competitor mention/citation data. Internally queries execution dates then the `brand_vs_competitors_by_date` view. | <ul><li><code>siteId</code> : <code>c2473d89-e997-458d-a86d-b4096649c12b</code> (required)</li><li><code>startDate</code> : <code>2026-01-01</code> (optional)</li><li><code>endDate</code> : <code>2026-03-31</code> (optional)</li><li><code>model</code> : <code>chatgpt</code> (optional)</li><li><code>categoryName</code> : <code>SEO</code> (optional)</li><li><code>regionCode</code> / <code>region</code> : <code>US</code> (optional)</li></ul> | [§13](#13-brand-vs-competitors) | [brand-vs-competitors-api.md](brand-vs-competitors-api.md) |

---

Expand Down Expand Up @@ -362,6 +363,27 @@ Illustrative JSON; real data varies by org and filters.
}
```

### 13. Brand vs competitors

```json
{
"competitorData": [
{
"siteId": "c2473d89-e997-458d-a86d-b4096649c12b",
"brandId": "019cb903-1184-7f92-8325-f9d1176af316",
"brandName": "Acme Corp",
"model": "chatgpt",
"executionDate": "2026-03-01",
"categoryName": "SEO",
"regionCode": "US",
"competitor": "Competitor Inc",
"totalMentions": 42,
"totalCitations": 7
}
]
}
```

---

## Authentication and errors
Expand Down
142 changes: 142 additions & 0 deletions docs/llmo-brandalf-apis/brand-vs-competitors-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Brand vs Competitors API

Returns aggregated competitor mention/citation data for a site. Queries the `brand_vs_competitors_by_date` view directly with date-range filters. Supports an `aggregate` mode that rolls up across category/region for chart-ready totals.

---

## API Paths

| Method | Path | Description |
|--------|------|-------------|
| GET | `/org/:spaceCatId/brands/all/brand-presence/brand-vs-competitors` | Competitor data for all brands |
| GET | `/org/:spaceCatId/brands/:brandId/brand-presence/brand-vs-competitors` | Competitor data for a specific brand |

**Path parameters:**
- `spaceCatId` — Organization ID (UUID)
- `brandId` — `all` (all brands) or a specific brand UUID

---

## Query Parameters

| Parameter | Aliases | Type | Required | Default | Description |
|-----------|---------|------|----------|---------|-------------|
| `siteId` | `site_id` | string (UUID) | **Yes** | — | Site to get competitor data for |
| `startDate` | `start_date` | string (YYYY-MM-DD) | No | 28 days ago | Start of date range |
| `endDate` | `end_date` | string (YYYY-MM-DD) | No | today | End of date range |
| `model` | — | string | No | `chatgpt` | LLM model |
| `categoryName` | `category_name` | string | No | — | Filter by category name |
| `regionCode` | `region_code`, `region` | string | No | — | Filter by region code |
| `aggregate` | — | boolean | No | `false` | Roll up across categoryName/regionCode |

---

## Sample URLs

**All brands, default date range:**
```
GET /org/44568c3e-efd4-4a7f-8ecd-8caf615f836c/brands/all/brand-presence/brand-vs-competitors?siteId=c2473d89-e997-458d-a86d-b4096649c12b
```

**Single brand with date range and filters:**
```
GET /org/44568c3e-efd4-4a7f-8ecd-8caf615f836c/brands/019cb903-1184-7f92-8325-f9d1176af316/brand-presence/brand-vs-competitors?siteId=c2473d89-e997-458d-a86d-b4096649c12b&startDate=2026-01-01&endDate=2026-03-31&categoryName=SEO&regionCode=US
```

**Aggregated for Market Tracking chart (one row per competitor per week):**
```
GET /org/44568c3e-efd4-4a7f-8ecd-8caf615f836c/brands/all/brand-presence/brand-vs-competitors?siteId=c2473d89-e997-458d-a86d-b4096649c12b&aggregate=true
```

---

## Internal Query (PostgREST)

Single query against the `brand_vs_competitors_by_date` VIEW with date-range filters:

- Selects: `site_id`, `brand_id`, `brand_name`, `model`, `execution_date`, `category_name`, `region_code`, `competitor`, `total_mentions`, `total_citations`
- Filters: `organization_id`, `site_id`, `model`, `execution_date` (gte/lte date range)
- Optional filters: `brand_id`, `category_name`, `region_code`
- Row limit: 5000

The VIEW is a regular (non-materialized) view — PostgreSQL pushes WHERE clauses through the GROUP BY into partition-pruned, index-covered scans on the source tables.

The underlying VIEW aggregates `executions_competitor_data` joined with `brand_presence_executions` and `organizations`, grouping by competitor (using `COALESCE(parent_company, competitor)` for fallback).

### Aggregation mode

By default, the response returns rows at `(competitor, executionDate, categoryName, regionCode)` granularity.

With `aggregate=true`, the server rolls up across `categoryName`/`regionCode` and returns one row per `(competitor, executionDate)` — the shape the **Market Tracking chart** needs directly. Aggregated rows omit `categoryName` and `regionCode`.

Category/region filters still apply *before* aggregation, so `aggregate=true&categoryName=SEO` returns chart-ready totals scoped to the SEO category.

---

## Response Format

**Default** (per category/region):

```json
{
"competitorData": [
{
"siteId": "c2473d89-e997-458d-a86d-b4096649c12b",
"brandId": "019cb903-1184-7f92-8325-f9d1176af316",
"brandName": "Acme Corp",
"model": "chatgpt",
"executionDate": "2026-03-01",
"categoryName": "SEO",
"regionCode": "US",
"competitor": "Competitor Inc",
"totalMentions": 42,
"totalCitations": 7
}
]
}
```

**With `aggregate=true`** (rolled up — no `categoryName`/`regionCode`):

```json
{
"competitorData": [
{
"siteId": "c2473d89-e997-458d-a86d-b4096649c12b",
"brandId": "019cb903-1184-7f92-8325-f9d1176af316",
"brandName": "Acme Corp",
"model": "chatgpt",
"executionDate": "2026-03-01",
"competitor": "Competitor Inc",
"totalMentions": 120,
"totalCitations": 18
}
]
}
```

---

## Error Responses

| Status | Condition |
|--------|-----------|
| 400 | `siteId` not provided |
| 400 | Organization not found |
| 400 | PostgREST query error |
| 403 | User does not belong to the organization |
| 403 | Site does not belong to the organization |

---

## Related APIs

- [Brand Presence Market Tracking Trends API](market-tracking-trends-api.md) — Weekly brand mentions/citations with competitor totals
- [Share of Voice API](share-of-voice-api.md) — Topic-level SOV (RPC-backed)
- [Brand Presence Filter Dimensions API](filter-dimensions-api.md) — Filter dropdown values

---

## Authentication

Requires valid authentication (JWT, IMS, or API key) with access to the organization.
2 changes: 2 additions & 0 deletions docs/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@ paths:
$ref: './llmo-api.yaml#/site-llmo-edge-optimize-routing'
/sites/{siteId}/llmo/strategy:
$ref: './llmo-api.yaml#/llmo-strategy'
/org/{spaceCatId}/brands/{brandId}/brand-presence/brand-vs-competitors:
$ref: './llmo-api.yaml#/llmo-brand-vs-competitors'
/sites/{siteId}/reports:
$ref: './site-api.yaml#/site-reports'
/sites/{siteId}/reports/{reportId}:
Expand Down
95 changes: 95 additions & 0 deletions docs/openapi/llmo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1944,3 +1944,98 @@ llmo-edge-optimize-status:
$ref: './responses.yaml#/500'
security:
- api_key: [ ]

llmo-brand-vs-competitors:
parameters:
- name: spaceCatId
in: path
required: true
description: Organization ID (UUID)
schema:
type: string
format: uuid
- name: brandId
in: path
required: true
description: Brand UUID or 'all' for all brands
schema:
type: string
get:
tags:
- llmo
summary: Get brand vs competitors aggregated data
description: |
Returns aggregated competitor mention/citation data for a site.
Queries the brand_vs_competitors_by_date view directly with date-range
filters. Response rows are at (competitor, executionDate, categoryName,
regionCode) granularity — aggregate across categoryName/regionCode
client-side for chart totals.
operationId: getBrandVsCompetitors
parameters:
- name: siteId
in: query
required: true
description: Site UUID (required)
schema:
type: string
format: uuid
- name: startDate
in: query
required: false
description: Start of date range (YYYY-MM-DD). Defaults to 28 days ago.
schema:
type: string
format: date
- name: endDate
in: query
required: false
description: End of date range (YYYY-MM-DD). Defaults to today.
schema:
type: string
format: date
- name: model
in: query
required: false
description: LLM model name
schema:
type: string
default: chatgpt
- name: categoryName
in: query
required: false
description: Filter by category name
schema:
type: string
- name: regionCode
in: query
required: false
description: Filter by region code (e.g. US, EU)
schema:
type: string
- name: aggregate
in: query
required: false
description: >
When true, rolls up across categoryName/regionCode to return one row
per (competitor, executionDate). Aggregated rows omit categoryName and
regionCode. Use for Market Tracking chart totals.
schema:
type: boolean
default: false
responses:
'200':
description: Aggregated competitor data per execution date
content:
application/json:
schema:
$ref: './schemas.yaml#/BrandVsCompetitorsResponse'
'400':
$ref: './responses.yaml#/400'
'401':
$ref: './responses.yaml#/401'
'403':
$ref: './responses.yaml#/403'
'500':
$ref: './responses.yaml#/500'
security:
- api_key: [ ]
50 changes: 50 additions & 0 deletions docs/openapi/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6764,3 +6764,53 @@ PresignedUrlResponse:
- presignedUrl
- expiresAt
additionalProperties: false

BrandVsCompetitorsResponse:
type: object
required:
- competitorData
additionalProperties: false
properties:
competitorData:
type: array
description: >
Aggregated competitor metrics per execution date.
When aggregate=true, rows are rolled up across categoryName/regionCode
and those fields are omitted.
items:
type: object
required:
- siteId
- brandId
- brandName
- model
- executionDate
- competitor
- totalMentions
- totalCitations
properties:
siteId:
type: string
format: uuid
brandId:
type: string
format: uuid
brandName:
type: string
model:
type: string
executionDate:
type: string
format: date
categoryName:
type: string
description: Present only when aggregate is not set (omitted when aggregate=true)
regionCode:
type: string
description: Present only when aggregate is not set (omitted when aggregate=true)
competitor:
type: string
totalMentions:
type: integer
totalCitations:
type: integer
Loading
Loading