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

# Journey and campaign analytics

> Query attributed revenue, estimated ROAS, and delivery rates by WhatsApp template category.

Use the [analytics query endpoint](/api-reference/analytics/query) to report on journeys and campaigns over a selected period. The same request shapes are available through the MCP `query_analytics` tool.

## Before you start

Create an [API client v2](/api-reference/authentication/api-client-v2) with `analytics.query` and the datasets you need:

| Dataset                 | Reports                                                 |
| ----------------------- | ------------------------------------------------------- |
| `marketing_attribution` | Attributed revenue, orders, and estimated ROAS.         |
| `outbound_messages`     | Message counts and delivery rates by template category. |

Use [Find entity IDs](/api-reference/analytics/members) to resolve campaign or journey names. Replace the example UUIDs below with IDs from your workspace. The [catalog](/api-reference/analytics/catalog) lists query shapes available to your client.

## Attributed revenue and estimated ROAS

Send this JSON body to `POST /platform-api/v1/analytics/query`:

```json theme={null}
{
  "dataset": "marketing_attribution",
  "measures": ["attribution_revenue", "estimated_roas"],
  "dimensions": ["time_bucket", "entity_type", "entity_id"],
  "filters": {
    "campaign_ids": ["22222222-2222-4222-8222-222222222222"]
  },
  "time": {
    "start": "2026-09-01T00:00:00Z",
    "end": "2026-09-07T23:59:59.999Z",
    "timezone": "UTC",
    "grain": "day",
    "anchor": "order_created_at"
  },
  "result_format": "rows",
  "limit": 100
}
```

For a journey, replace `campaign_ids` with `journey_ids`. To compare selected campaigns and journeys, provide both arrays. Attribution returns either selected entity type, further restricted by `entity_types` if provided. An empty ID array imposes no restriction; each array accepts up to 100 UUIDs.

An example row in `rows`:

```json theme={null}
{
  "time_bucket": "2026-09-03",
  "entity_type": "campaign",
  "entity_id": "22222222-2222-4222-8222-222222222222",
  "attribution_type": "delivery",
  "window_hours": 72,
  "attribution_revenue": {
    "by_currency": [{"currency": "INR", "revenue": 910, "orders": 2}]
  },
  "estimated_roas": {
    "by_currency": [{"currency": "INR", "roas": 10}]
  }
}
```

These illustrative values use INR 91 in estimated messaging cost. `roas: 10` means ten units of attributed revenue per unit of estimated spend; it is a ratio, not a percentage.

### Attribution settings and pagination

* Omit `filters.attribution_type` and `filters.window_hours` to use your workspace attribution settings. Override the event with `delivery`, `read`, or `clicked`, and the lookback with 1–8760 hours. The lookback is separate from the reporting period.
* `meta.applied_filters` reports the effective attribution event, lookback, and entity types even when no rows match.
* Use the exact dimensions `["time_bucket", "entity_type", "entity_id"]`. Choose `day`, `week`, or `month`; `order_created_at` is the supported time anchor.
* Request `attribution_revenue`, `estimated_roas`, or both. The existing `attribution_cost` measure can also be requested. Only requested measures are returned.
* Results use a fixed order by time bucket, entity type, and entity ID. Custom sort and `summary` are unsupported. Follow `meta.cursor` with the same query until it is null. The default and maximum page size is 1000.

Selecting one entity does not reassign orders that were attributed elsewhere. Attribution selects the winning eligible interaction before applying campaign/journey ID filters.

### How to interpret estimated ROAS

Estimated ROAS divides attributed revenue by estimated messaging cost **in the same currency**. The existing estimate covers delivered/read WhatsApp marketing messages at INR 0.91 and utility messages at INR 0.15. Authentication messages and other channels are outside this estimate. These are estimation inputs, not actual invoiced charges or a pricing quotation.

| Situation in a bucket                                    | `roas`                            |
| -------------------------------------------------------- | --------------------------------- |
| Revenue and positive estimated cost in the same currency | Revenue divided by cost.          |
| Positive estimated cost with no revenue in any currency  | `0`.                              |
| Zero or missing estimated cost                           | `null`.                           |
| Revenue only in USD and estimated cost only in INR       | `null` for both currency entries. |

No currency conversion is performed. Keep currency breakdowns separate. Revenue uses gross attributed order totals and includes cancelled/refunded orders; it is not net revenue. Do not average daily ROAS values to calculate a period's ROAS. Where matching revenue and cost are available, sum each in the same currency and divide those totals.

Revenue buckets use order creation time; spend buckets use message send time. A reporting period can therefore include spend for orders attributed in another period. Campaign and journey spend can overlap, so do not add their costs together as a workspace total.

## Delivery by template category

Send this body to the same query endpoint:

```json theme={null}
{
  "dataset": "outbound_messages",
  "measures": ["sent_count", "delivered_like_count", "read_count", "failed_count", "delivery_rate"],
  "dimensions": ["journey_id", "template_category"],
  "filters": {
    "journey_ids": ["33333333-3333-4333-8333-333333333333"]
  },
  "time": {
    "start": "2026-09-01T00:00:00Z",
    "end": "2026-09-07T23:59:59.999Z",
    "timezone": "UTC",
    "grain": "day",
    "anchor": "sent_at"
  },
  "result_format": "rows"
}
```

An example row in `rows`:

```json theme={null}
{
  "time_bucket": "2026-09-03",
  "journey_id": "33333333-3333-4333-8333-333333333333",
  "template_category": "MARKETING",
  "sent_count": 100,
  "delivered_like_count": 80,
  "read_count": 50,
  "failed_count": 5,
  "delivery_rate": 0.8
}
```

Choose one of these dimension lists, in the order shown. `time_bucket` is added to each response row automatically; do not include it in these request dimensions.

| Dimensions                             | Grouping                                    |
| -------------------------------------- | ------------------------------------------- |
| `["template_category"]`                | Category across matching outbound messages. |
| `["campaign_id", "template_category"]` | Campaign and category.                      |
| `["journey_id", "template_category"]`  | Journey and category.                       |

Campaign/journey groupings include only messages linked to that entity type. Category-only grouping also includes messages without campaign or journey IDs.

You can request any nonempty subset of the five measures in the example. `sent_count` counts messages whose current status is sent, delivered, or read. `delivered_like_count` counts delivered or read; `read_count` counts read; `failed_count` counts failed.

`delivery_rate` is `delivered_like_count / sent_count`: **0–1**, so `0.8` means 80%. A zero denominator returns `0`. Failed messages are reported separately and are not in this denominator. For a combined delivery rate, divide the summed counts; do not average the row rates.

### Category and filter rules

* Categories are `MARKETING`, `UTILITY`, `AUTHENTICATION`, and `UNKNOWN`. They reflect the **current WhatsApp template category**, so reclassification can change historical reports.
* Missing/deleted templates and non-WhatsApp messages use `UNKNOWN`. For a WhatsApp-only report, add `channel_kinds: ["whatsapp"]` to `filters`.
* `template_categories` filters categories and requires one of the category groupings above. Category values are case-sensitive.
* `campaign_ids`, `journey_ids`, `has_campaign`, `has_journey`, `channel_ids`, and `channel_kinds` combine with AND on message rows. With both ID arrays supplied, each message must match both.
* Results contain observed buckets only, with no empty-date/category rows added. `summary` and cursors are unsupported. Omit `limit` to receive all matching buckets; a limit truncates the result without a next page.

## Time windows and errors

Use ISO 8601 timestamps with an explicit offset. Timestamps without an offset are treated as UTC. `time.timezone` controls day/week/month grouping; it does not reinterpret start/end offsets.

Revenue and category delivery include both start and end timestamps. Estimated cost includes the start and excludes the end. Category delivery uses send time, falling back to message creation time for messages not yet sent. These boundaries and different event times matter when reconciling reports.

Marketing attribution and category queries reject reversed windows, unknown timezones, `aggregation`, and `business_hours`. Unsupported or misspelled filters return an error rather than an unfiltered total. A missing scope/dataset grant returns `403`; invalid combinations return `400`, and malformed fields return `422`.

For MCP examples and the distinction between date-bounded analytics and lifetime product overviews, see [Journey and campaign tools](/mcp/journeys-campaigns).
