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

# Conventions & gotchas

> Read once, paste into your AI assistant, and skip the most common day-one mistakes.

## Authentication

Every request needs your API key in the `X-API-Key` header. The Python
SDK reads it from the `PROFOUND_API_KEY` env var.

<CodeGroup>
  ```python Python theme={null}
  import os
  from profound import Profound

  client = Profound(api_key=os.environ["PROFOUND_API_KEY"])
  categories = client.organizations.categories.list()
  ```

  ```bash curl theme={null}
  curl https://api.tryprofound.com/v1/org/categories \
    -H "X-API-Key: your_api_key_here"
  ```
</CodeGroup>

Generate a key in the app under **Settings → API Keys**. Treat it like a
password — it has full read access to your org's analytics data.

## Rate limit

**600 requests per hour, per key.** Anything above returns `429 Too Many
Requests`. Cache responses where you can and batch period-over-period or
multi-asset queries instead of fanning out.

## `end_date` is exclusive — add one day

`end_date` is parsed at the **start** of that day in **Eastern Time**,
so it's excluded from the response. To include all of `May 10`, send
`end_date="2026-05-11"`.

| Window you want to display           | What to send                                     |
| ------------------------------------ | ------------------------------------------------ |
| `May 4 → May 10` (7 days, inclusive) | `start_date="2026-05-04", end_date="2026-05-11"` |
| `April 1 → April 30` (full month)    | `start_date="2026-04-01", end_date="2026-05-01"` |

The `date_interval` buckets (`"day"` / `"week"` / `"month"`) are also
computed in ET.

## Read column positions from `info.query`, not your request

Each row in the response packs its `metrics` and `dimensions` as arrays.
The order of values in those arrays comes from `info.query.metrics` and
`info.query.dimensions` — **not** from the order you sent in the
request. Always look it up:

```python theme={null}
order   = res.info.query["metrics"]
i_score = order.index("visibility_score")
score   = res.data[0].metrics[i_score]
```

A response always looks like this:

```json theme={null}
{
  "info": {
    "total_rows": 12345,
    "query": {
      "metrics": ["visibility_score", "share_of_voice"],
      "dimensions": ["asset_name"]
    }
  },
  "data": [
    { "metrics": [0.42, 0.17], "dimensions": ["<your-asset>"] }
  ]
}
```

## Period-over-period deltas are client-side

The API doesn't return change vs the previous period. Run the same call
twice — current window and a prior window of equal length — and subtract.

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

A call with `dimensions=["date"]` returns one row per day. A call without
`date` returns one row for the whole window. These are **different
numbers**: the period score is traffic-weighted, an average of daily
rows is not. Use the no-`date` call for headlines; use the with-`date`
call for charts. Never derive one from the other.

## Pagination

Default `pagination.limit` is `100`. Max is `50,000`. Use
`info.total_rows` (returned on every response) to decide whether to
paginate. Almost all queries fit in a single 50k page; only heavy
`dimensions=["url", ...]` citation queries usually need a second page.

```python theme={null}
pagination={"limit": 50000, "offset": 0}
```

If you do need more, increment `offset` by `limit` until you've covered
`total_rows`.

## Filters

Every report endpoint accepts a `filters` array of `{field, operator,
value}` objects:

```json theme={null}
{ "field": "asset_name", "operator": "is", "value": "<your-asset-name>" }
```

| Operator                    | What it does                         |
| --------------------------- | ------------------------------------ |
| `is`                        | Exact match (scalar value)           |
| `not_is`                    | Not equal                            |
| `in`                        | Match any value in an array          |
| `not_in`                    | Match none of the values in an array |
| `contains`                  | Substring match (case-sensitive)     |
| `contains_case_insensitive` | Substring match (case-insensitive)   |
| `matches`                   | Regex match                          |

`prompt_type` (with values like `"visibility"`) maps to the app's view
toggles. Send `prompt_type=visibility` on Citations / Visibility queries
to mirror the default UI scope.

## Error responses

| Status | Meaning                    | What to check                                             |
| ------ | -------------------------- | --------------------------------------------------------- |
| `400`  | Validation error           | The response body's `detail` / `errors` field.            |
| `401`  | API key missing or invalid | The `X-API-Key` header; whether the key has been revoked. |
| `403`  | Key valid but no access    | The `category_id` may belong to a different org.          |
| `404`  | Wrong path                 | Typo or wrong API version.                                |
| `429`  | Rate limited               | Back off; throttle to ≤600/hr.                            |
| `5xx`  | Server error               | Retry with exponential backoff.                           |

## Timezones

All bucketing happens in **Eastern Time**. A "last 7 days" range anchored
to your local clock can land on a different ET day than you expect.
Anchor scheduled jobs to ET:

```python theme={null}
from datetime import datetime
from zoneinfo import ZoneInfo

today_et = datetime.now(ZoneInfo("America/New_York")).date()
```
