# Multihub Reporting API — instructions for AI assistants

You are helping someone read their properties' marketing performance data from the
Multihub Reporting API. The token is read-only on this API: queries work, and every
mutation is refused.

## Connection

- Endpoint: `POST https://api.multihub.io/graphql` with a JSON body
  `{"query": "...", "variables": {...}}`.
- Auth header: `Authorization: Bearer $MULTIHUB_TOKEN`. The token is in the
  `MULTIHUB_TOKEN` environment variable. Always reference the variable in commands.
  Never print it, log it, or write it into a file. If the variable is not set, stop
  and point the user to https://api.multihub.io/assets/docs/ai-assistants.html#token.
  Do not ask them to paste the token into the chat.
- Documentation: https://api.multihub.io/assets/docs/index.html (overview and data
  caveats) and https://api.multihub.io/assets/docs/query-reference.html (every
  argument, operator, grain and aggregation). Read the query reference before your
  first `reporting_data` query if you are able to fetch web pages.

## Working method

1. Run `reporting_datasets` once and keep the result. It lists every dataset with its
   dimensions (what you can group and filter by), its metrics (what you can measure),
   the integrations it needs, and a description carrying the caveats specific to it.
   Read a dataset's description before you query it.
2. Run `companies` once. Reporting rows carry ids, not names; this query maps
   `company_id` and `multihub_property_id` to names. Keep every id as a string. They
   are 16 digits, larger than a double holds exactly.
3. Query `reporting_data` with an explicit, bounded date range. Present results as a
   table using property or company names, and state the date range and the dataset
   you used.

Template (macOS, Linux, or Git Bash on Windows):

```sh
curl -sS -X POST https://api.multihub.io/graphql \
  -H "Authorization: Bearer $MULTIHUB_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "query": "query($g:[JSON],$f:[JSON]){ reporting_data(dataset:\"paid_campaigns_daily\", metrics:[\"Paid Spend\",\"Paid Clicks\"], group_by:$g, filters:$f, page_size:5000) { rowCount totalRows cursor rows } }",
  "variables": {
    "g": ["property", {"dim": "date", "grain": "month"}],
    "f": [
      {"dim": "date", "op": ">=", "value": "2026-01-01"},
      {"dim": "date", "op": "<",  "value": "2026-07-01"}
    ]
  }
}
JSON
```

In Windows PowerShell there is no heredoc: write the JSON body to a file and send it with
`curl.exe ... -H "Authorization: Bearer $env:MULTIHUB_TOKEN" --data-binary "@body.json"`.

Catalog and names:

```graphql
{ reporting_datasets { name description required_integrations last_refresh_at
    dimensions { id name type grains } metrics { name unit aggregations default_aggregation } } }

{ companies { company_id company_name properties { multihub_property_id property_name } } }
```

## Rules that produce wrong numbers if ignored

Each dataset's `description` and each metric's description in the catalog carry the
caveats specific to them (which metrics are not additive, which dataset to total from,
what a balance column means). Obey them. The rules below are the ones the catalog does
not carry:

- Data is complete through today − 3. Bound every date window at today − 3 and never
  leave the end date open. Say which window you used.
- `date` arrives as an object, `{"value": "2026-01-01"}`. Some metrics arrive as
  quoted numbers. Convert both before you sort, sum or compare.
- Grouping by `property`, `company` or `agency` expands a row that belongs to more than
  one entity into one row per entity, so a property-grouped total is higher than the
  same query ungrouped. Take portfolio totals from the ungrouped query; use the grouped
  query for the breakdown.
- GA4 totals include the `Unassigned` channel. Filter it out only when reconciling
  against a GA4 channel report, and say that you did.
- Rows are identifiers only. Never show a raw id where a name is available.
- A dataset the user's properties have no connection for returns zero rows. That is
  not an error: name the missing integration from `required_integrations`.

## Limits

One `reporting_data` per request. Up to 10,000 rows per response and six `group_by`
dimensions per query. When `cursor` is non-null, send it back to fetch the next page
and keep going until it is null. Run one or two queries at a time, not many in
parallel.
