> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rankraft.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Visibility over time

> Daily, weekly or monthly Visibility Score lines for your brand, optionally one per engine.

A trend line is the visibility report with `date` in `group_by`. That gives **daily** points. There are no week or month buckets, so weekly and monthly points are one call per week or month, each computed from every answer in it.

> [!WARNING]
>
> Don't average daily rows to make a weekly or monthly point: coverage and unequal answer counts make the average a different number. See [Headline and daily together](/cookbook/headline-and-daily).

## How this example works

1. **Daily:** one call with `group_by: ["date"]` (add `"platform"` for a line per engine). One row per day with answers.
2. **Weekly or monthly:** split the range into buckets and make one call per bucket with no `group_by`. Thirteen weeks is thirteen calls, well inside 120 a minute; if you do hit the limit, the SDKs wait out `Retry-After` and retry.
3. **Write a CSV** you can chart anywhere.

```js JavaScript theme={"system"}
import { writeFileSync } from "node:fs";
import { RankRaft } from "rankraft-js";

const rr = new RankRaft();
const site_id = process.env.RANKRAFT_SITE_ID;
const metrics = ["visibility_score", "mention_rate", "citation_rate", "answers"];

// Daily, one line per engine.
const daily = await rr.reports
  .visibility({ site_id, start_date: "2026-07-01", end_date: "2026-09-30", group_by: ["date", "platform"], metrics })
  .all();

// Monthly: one call per calendar month.
const months = [["2026-07-01", "2026-07-31"], ["2026-08-01", "2026-08-31"], ["2026-09-01", "2026-09-30"]];
const monthly = await Promise.all(
  months.map(async ([start_date, end_date]) => {
    const page = await rr.reports.visibility({ site_id, start_date, end_date, metrics });
    return { month: start_date.slice(0, 7), ...(page.data[0] ?? { visibility_score: null, answers: 0 }) };
  }),
);

writeFileSync(
  "visibility-daily.csv",
  ["date,platform,visibility_score,mention_rate,citation_rate,answers"]
    .concat(daily.map((r) => [r.date, r.platform, r.visibility_score, r.mention_rate, r.citation_rate, r.answers].join(",")))
    .join("\n"),
);
for (const m of monthly) console.log(m.month, m.visibility_score, `(${m.answers} answers)`);
```

```python Python theme={"system"}
import os
from datetime import date, timedelta
from rankraft import RankRaft

SITE_ID = os.environ["RANKRAFT_SITE_ID"]
METRICS = ["visibility_score", "mention_rate", "citation_rate", "answers"]

def weeks(start: date, end: date):
    """Monday-to-Sunday buckets covering start..end (end inclusive)."""
    d = start - timedelta(days=start.weekday())
    while d <= end:
        yield max(d, start), min(d + timedelta(days=6), end)
        d += timedelta(days=7)

with RankRaft() as rr:
    # Daily, one line per engine. Needs the pandas extra.
    daily = rr.reports.visibility(
        site_id=SITE_ID, start_date="2026-07-01", end_date="2026-09-30",
        group_by=["date", "platform"], metrics=METRICS,
    ).to_pandas(all_pages=True)
    daily.to_csv("visibility-daily.csv", index=False)

    # Weekly: one call per week.
    for start, end in weeks(date(2026, 7, 1), date(2026, 9, 30)):
        page = rr.reports.visibility(site_id=SITE_ID, start_date=start, end_date=end, metrics=METRICS)
        row = page.data[0] if page.data else None
        print(start, row["visibility_score"] if row else "-", f"({row['answers'] if row else 0} answers)")
```

```bash curl theme={"system"}
curl https://api.rankraft.ai/v1/reports/visibility \
  -H "X-API-Key: $RANKRAFT_API_KEY" -H "Content-Type: application/json" \
  -d '{"site_id":"'$RANKRAFT_SITE_ID'","start_date":"2026-07-01","end_date":"2026-09-30",
       "group_by":["date","platform"],"limit":100,
       "metrics":["visibility_score","mention_rate","citation_rate","answers"]}'
# Follow info.next_cursor until it is null, sending the same body plus "cursor".
```

## Notes

* **Partial buckets.** The first and last week of a range are often shorter than seven days. Label them, or start the range on a Monday.
* **Small days are noisy.** One engine on one day may rest on a handful of answers. Plot `answers` beside the score, or use weekly points.
* **92 days per call.** For a longer daily history, request quarter by quarter and concatenate; daily rows never overlap.
* **Engines started on different days** have no rows before their first scan. A missing row means no data, not a score of zero.

## Common follow-ups

> **[Compare competitors over time](/cookbook/compare-competitors)**
>
> Add a line per competitor.

> **[Segment by engine, category or prompt](/cookbook/segment-visibility)**
>
> Break one period down instead of over time.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.