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

# Conventions & gotchas

> Dates, scales, ordering and pagination, and the mistakes that produce numbers that look right and are not.

Every recipe in the cookbook follows these rules. Most wrong numbers come from breaking one of them, and none of them raises an error when you do.

## Dates are inclusive UTC days

`start_date` and `end_date` are `YYYY-MM-DD` and **both are included**. September is `2026-09-01` to `2026-09-30`, not `2026-10-01`.

* Answer reports (visibility, citations, sentiment, answers) file each answer under the **UTC** day it was collected.
* Traffic reports (AI crawlers, AI referrals) take an optional `timezone` (an IANA name such as `Asia/Karachi`, default `UTC`) for the day boundaries and the `date` buckets.
* An answer report covers at most **92 days**, a traffic report at most **400 days**. For a longer history, make one call per quarter.

To compare with the previous period, use a window of the **same length** that ends the day before the current one starts:

```js JavaScript theme={"system"}
const DAY = 86_400_000;
const iso = (d) => d.toISOString().slice(0, 10);

function previous(start, end) {
  const s = Date.parse(`${start}T00:00:00Z`);
  const e = Date.parse(`${end}T00:00:00Z`);
  const days = (e - s) / DAY + 1;
  return [iso(new Date(s - days * DAY)), iso(new Date(s - DAY))];
}

previous("2026-09-01", "2026-09-30"); // ["2026-08-02", "2026-08-31"]
```

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

def previous(start: date, end: date) -> tuple[date, date]:
    days = (end - start).days + 1
    return start - timedelta(days=days), start - timedelta(days=1)

previous(date(2026, 9, 1), date(2026, 9, 30))  # (2026-08-02, 2026-08-31)
```

## Know the scale of each number

| Field | Scale |
| - | - |
| `visibility_score` | **0–100** |
| `mention_rate`, `citation_rate`, `prompt_coverage`, `share_of_voice`, `citation_share`, sentiment `positive` / `neutral` / `negative` | **0–1** fractions |
| `average_position` | 1 = named first. Lower is better. `null` when the brand was never named |
| `answers`, `citations`, `hits`, `visits` | counts |

Multiply a fraction by 100 to show a percentage. Report a change in a rate as **percentage points** (0.42 → 0.47 is +5 pp), not as a percentage of a percentage.

## Don't average daily rows to get a period score

A monthly Visibility Score is computed from every answer in the month. The average of thirty daily scores is a different number: it gives a day with three answers the same weight as a day with forty, and `prompt_coverage` in particular does not average at all (a prompt named on one day of the month counts as covered for the month).

Ask for the period in **one call without `date` in `group_by`**. Use daily rows for the trend line only. The same applies to weeks and months: there are no week or month buckets, so make one call per week or per month. See [Headline and daily together](/cookbook/headline-and-daily).

Counts are different: `answers` and `citations` add up across days.

## Changes are computed client-side

No endpoint returns a delta. Make two calls with windows of equal length and subtract. See [Visibility Score with change](/cookbook/visibility-score).

## Rows have named fields

Every row is an object with named fields, so you never read a value by position. Grouped dimensions appear on the row under their own names: `date`, `platform`, `category`, and `prompt` as `{ id, text }`. Visibility rows always carry `brand: { name, owned }` and `rank`.

## `citation_share` is computed after the filter

In the citations report, `citation_share` is each row's share of the citations **that pass your filter**, within its group. Filter on `owner = own` and your own domains will always add up to 1. To get the share of *all* citations that went to you, request the report unfiltered and sum the rows where `owner` is `own`. See [Owned citation share](/cookbook/owned-citation-share).

## Ordering

* Visibility rows are sorted by the grouped dimensions (dates ascending), then by `rank`. Pass `sort: { field, dir }` to sort by a metric instead.
* Citation rows are sorted by the grouped dimensions, then by `rank`: most answers citing it first.
* Within each group, `rank` restarts at 1.

Sort again in your own code when you need a particular order. It costs nothing and makes the direction explicit.

## Pagination

Reports return at most 100 rows a page (traffic reports 500, answers 50). `info.next_cursor` is `null` on the last page; otherwise pass it back as `cursor` with **the same query**. A cursor is bound to its query, so changing a date or a filter with an old cursor is refused with `400 invalid_cursor`.

The SDKs handle this for you:

| | First page only | Every row |
| - | - | - |
| JavaScript | `const page = await rr.reports.visibility({...})` | `for await (const row of rr.reports.visibility({...}))` or `.all()` |
| Python | `page = rr.reports.visibility(...)`; `for row in page` | `page.auto_paging()`, `page.all()`, `page.to_pandas(all_pages=True)` |

A leaderboard or a top-N list usually fits on one page. Anything you sum or join across rows, such as an owned share, needs **every** page.

## Google pages with no AI Overview are left out

About three in ten Google result pages show no AI Overview at all. Those are a finding about the question, not answers that failed to name you, so they are excluded from every rate and from `answers` on `ai_overviews` and `ai_mode`. A day with few answers on those engines is usually this, not a broken scan.

## Engines and prompts start on different days

An engine added to the site, or a prompt switched on, produces data from its first scan. Check `answers` before reading a rate: a rate over two answers is noise. Engine IDs are listed by [`GET /v1/platforms`](/api-reference/endpoints/list-platforms).

## Rate limits

120 requests a minute per key, 20 of them writes. A `429` carries `Retry-After`; the SDKs wait exactly that long and retry. Cache anything that changes rarely, such as your site list and competitors. See [Rate limits](/api-reference/rate-limits).

## Keys stay on a server

The API sends no CORS headers on purpose. Call it from a server, a script or a notebook, never from code a browser downloads.


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