---
title: "Call analytics.query_metrics"
method: POST
path: "/api/v1/tools/analytics.query_metrics"
tags: ["analytics"]
---

# Call analytics.query_metrics

`POST /api/v1/tools/analytics.query_metrics`

## Request

- Base URL: `https://app.goosybear.ai`
- URL: `https://app.goosybear.ai/api/v1/tools/analytics.query_metrics`
- Auth: HTTP bearer

'how many visitors did we get last week?', 'what's our conversion rate?' — read this workspace's analytics numbers over a window: the same figures, from the same reads, the analytics pages show. Name dictionary keys (analytics.list_available_metrics lists them) and a preset period or an explicit from/to; set compare to also read the preceding window. Every row carries a state: empty is a final answer, read_failed means nothing is known, and suppressed means a small-count privacy floor withholds the figure. It changes nothing. Not for which metrics exist or report here — that is `analytics.list_available_metrics`.

Every result names the workspace it ran in. Say which workspace the answer is about. When the account has more than one workspace and none is selected, this tool refuses with `workspace_ambiguous` and lists the choices — offer them, never pick one.

## Headers

- `x-request-id` string

## Request body

- object
  - `metric_keys` string[], required: Dictionary metric keys to read. Use analytics.list_available_metrics to discover them.
  - `period` string: The window ending at the start of today (today is excluded), as the span the member names, counted in whole days: 14d for two weeks, 30d for 30 days, 3d for the last three days. Three ids are the analytics page's own presets and read whole COMPLETE periods: 7d = the last 7 complete days, 28d = the last 4 complete weeks, 90d = the last 3 complete calendar months (92 days on a May-Aug read, not 90); use one of them only when the member's span is that one. Any other count, 1d to 366d, reads that many complete days. Omit when supplying from/to, which is for named calendar dates ('in March').
  - `from` string: Window start, ISO-8601 date or timestamp. Inclusive. Requires `to`.
  - `to` string: Window end, ISO-8601 date or timestamp. EXCLUSIVE.
  - `grain` 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'lifetime': Granularity the stored rows are read at. LEAVE THIS UNSET unless you have a specific reason: day is where the analytics store is written, and asking for a coarser grain reads a different set of rows that is usually empty. A window is summed from day rows regardless of how long it is, so a month-long window does NOT need grain=month.
  - `compare` boolean: Also read the immediately preceding window of the same number of COMPLETE periods, for a like-for-like comparison. On a preset that is a calendar step, so the preceding window holds the same number of weeks or months — not necessarily the same number of days.
  - `by` 'platform': Split each ad sum (ads_spend, ads_impressions, ads_clicks, ads_conversions) by ad platform — Meta, Google, LinkedIn, TikTok, Snapchat — each with its state: measured (a figure), quiet (connected, nothing ran), unread (we do not read that platform's spend yet) or not_connected. A platform with no figure is never a zero. Other keys come back unsplit. The split is read on a signed-in member's session only: a call made with an API key answers the unsplit figures and a split_note saying so.
  - `site` string: Narrow site_* metrics to one of the brand's Pages sites, by its name as the member says it, like Riverside Bakery, or by its id. The name is looked up in this workspace, so no `page.list` call is needed first. Add page_path for one page of it; a path (it starts with /) is always page_path, never site. Only with site_* keys.
  - `site_id` string, uuid: The same narrowing as site, by the site's id alone. Only with site_* keys.
  - `page_path` string: Narrow site_* metrics to one page by its path, like /pricing — that path on every site unless site is also given. Only with site_* keys.
  - `workspace` string: Which workspace to run in — its slug. Omit to use your default. With more than one reachable workspace and no default, the call is refused and the choices are listed.

## Response `200`

The call was admitted and dispatched. `ok` says whether the tool succeeded — a refusal the tool itself produced is still a 200, exactly as it is a successful JSON-RPC result over MCP.

- union
  - object: The tool ran and answered.
    - `ok` true, required
    - `window` object, required
      - `from` string, required
      - `to` string, required
      - `label` string, required
    - `previous_window` object, nullable, required
      - `from` string, required
      - `to` string, required
      - `label` string, required
    - `grain` string, required
    - `definition_version` integer, required
    - `min_delta_periods` integer, required
    - `metrics` object[], required
      - `key` string, required
      - `label` string, required
      - `module` string, required
      - `formula` string, required
      - `source` string, required
      - `provenance` string, required
      - `aggregation` string, required
      - `definition_version` integer, required
      - `is_directional` boolean, required
      - `honesty_note` string, nullable, required
      - `floor` object, nullable, required
        - `kind` string, required
        - `threshold` integer, required
      - `recompute_required` boolean, required
      - `current` object, required
        - `value` number, nullable, required
        - `display` string, required
        - `row_count` integer, required
        - `currency_code` string, nullable, required
        - `store` 'metric_rollups' | 'metric_snapshots' | 'ad_campaign_metrics' | 'page_view_daily' | 'none', required
        - `suppressed` string, nullable, required
      - `previous` object, nullable, required
        - `value` number, nullable, required
        - `display` string, required
        - `row_count` integer, required
        - `currency_code` string, nullable, required
        - `store` 'metric_rollups' | 'metric_snapshots' | 'ad_campaign_metrics' | 'page_view_daily' | 'none', required
        - `suppressed` string, nullable, required
      - `change` object, nullable, required
        - `absolute` number, required
        - `percent` number, nullable, required
      - `change_unavailable` string, nullable, required
      - `state` 'reported' | 'empty' | 'not_aggregatable' | 'suppressed' | 'read_failed', required
      - `by_platform` object[], nullable, required
        - `platform` string, required
        - `label` string, required
        - `state` 'measured' | 'quiet' | 'unread' | 'not_connected', required
        - `value` number, nullable, required
        - `display` string, required
        - `detail` string, required
    - `all_empty` boolean, required
    - `answer_note` string, required
    - `split_note` string, nullable, required
    - `site_filter` object, nullable, required
      - `site_id` string, nullable, required
      - `page_path` string, nullable, required
    - `workspace` string, required: The slug of the workspace this call ran in.
    - `working_in` object, required: Which workspace this call ran in, and how that was decided. Present on every workspace-scoped result.
      - `label` string, required: The chip words — e.g. "working in: Acme Main".
      - `note` string, required: Why this workspace — e.g. "your default", "this call only".
      - `workspace` string, required: The resolved workspace's slug.
      - `source` 'call-override' | 'key-pin' | 'stored-default' | 'sole-reachable', required: Which rung of the R7 precedence resolved the workspace.
  - ToolRefusal: A call that was admitted and dispatched, and did not succeed.
    - `ok` false, required
    - `code` string, required: The typed reason — e.g. `workspace_ambiguous`, `invalid_arguments`, or the tool's own failure code.
    - `message` string, required: A sentence a person can act on.
    - `workspaces` string[]: On a workspace refusal: the slugs this key may pass as `workspace`.
    - `issues` object[]: On `invalid_arguments`: which fields failed, and why.
      - `path` string, required
      - `message` string, required
    - `path` string: On an authoring refusal: where in your call's input it points, e.g. `steps.<key>.params.<field>`.
    - `fix` string: On an authoring refusal: the change to your call that would pass.
    - `instruction` string: On an authoring refusal: what to do next with it.

## Other responses

- `400`: The request could not be used: the body is present but not a JSON object (`tenant_api.malformed_body`); it is a top-level array carrying more messages than this endpoint accepts (`tenant_mcp.batch_too_large`); or the `Idempotency-Key` header is malformed (`tenant_api.idempotency_key_invalid`), disagrees with the body's `idempotency_key` (`tenant_api.idempotency_key_conflict`), or was sent to an action that cannot replay a retry (`tenant_api.idempotency_unsupported`). Nothing ran.
- `401`: No bearer credential, or one that did not resolve. Deliberately ONE undifferentiated answer for absent / unknown / revoked / expired, so the endpoint is never an oracle over the key space.
- `403`: The credential is good but the surface is not open to it — the platform or account API switch is off, or the holder lacks the `api.access` capability. The `code` says which.
- `404`: No such tool. The body names no tool and is identical for every unrecognised name.
- `429`: Over this key's per-minute cap. Carries `Retry-After` and `error.retry_after_seconds`.

---

[API](https://skmtc.dev/goosybear/apis/goosy-bear-api.md) · [All operations](https://skmtc.dev/goosybear/apis/goosy-bear-api/llms.txt) · [OpenAPI document](https://skmtc.dev/goosybear/apis/goosy-bear-api/revisions/f9b4aeb83edc?raw)
