Reports

Intro

Endpoint: https://api.omnisend.com/api/analytics/reports

What the Reports API is for

The Reports API provides aggregated, time-series analytics for your marketing data. It lets you answer questions like:

  • Campaign and automation workflow performance — How many messages were sent, opened, or clicked over a given period?
  • Sales attribution — How much revenue and how many orders can be attributed to specific campaigns or automation workflows?
  • Deliverability insights — What are my bounce, spam complaint, and unsubscribe rates?
  • Rate metrics — What are my open rate, click rate, or revenue per message sent?
📘

How results are grouped

Results are grouped by the date a campaign or automation workflow message was sent. If a campaign was sent on Monday, all opens, clicks, and orders attributed to that campaign appear under Monday — regardless of when those events actually happened.

This matches how Omnisend in-app reports work. If you need results grouped by event date instead (when the open or click actually occurred), use the Statistics API instead.

What this API is not for

  • Event-date analysis — This API groups by send date. If you need activity grouped by the date each event occurred, use the Statistics API.
  • Individual contact or event-level data — This API returns aggregated metrics, not per-recipient activity logs.
  • Real-time monitoring — Data is based on completed hours. The current hour may be incomplete (see Data freshness).
  • Audience growth tracking — Subscription and unsubscription counts by channel are only available in the Statistics API.

Available data at a glance

Metrics

Metrics are the values you want to measure. At least one metric is required per query. Duplicate metrics within a query are not allowed.

Delivery

MetricDescription
sentTotal messages sent
failedMessages that failed to deliver (bounces, invalid addresses, etc.)
failRateRatio of failed messages to sent messages
sentCostCost incurred for sending messages. SMS only.
markedAsSpamUniqueUnique messages marked as spam by recipients
markedAsSpamRateRatio of spam-marked messages to sent messages
unsubscribedUniqueUnique messages that resulted in an unsubscribe
unsubscribeRateRatio of unsubscribed messages to sent messages

Engagement

MetricDescription
openedUniqueUnique messages opened at least once
openedTotal opens, including repeat opens of the same message
openRateRatio of unique opens to sent messages
clickedUniqueUnique messages clicked at least once
clickedTotal clicks, including repeat clicks on the same message
clickRateRatio of unique clicks to sent messages

Revenue attribution

MetricDescription
attributedOrdersUniqueUnique messages with at least one attributed order
attributedOrdersTotal placed orders attributed to messages
attributedOrderRateRatio of messages with at least one order to sent messages
attributedRevenueTotal revenue from orders attributed to messages
attributedRevenuePerOrderAverage revenue per attributed order
attributedRevenuePerSentAverage revenue per sent message

Total shop sales (sent-based)

MetricDescription
totalOrdersPlaced orders from message recipients, counted regardless of attribution classification
totalRevenueRevenue from orders placed by message recipients, counted regardless of attribution classification

Channel differences: Most metrics apply across all channels (Email, SMS, Push). Exception:

  • sentCost — currently available for SMS only.

Dimensions

Dimensions let you break down metrics into groups. They are optional — omitting dimensions returns a single totals row with no breakdown. You can include a timestamp dimension (requires granularity) plus up to 2 non-timestamp dimensions per query.

DimensionDescriptionAvailable values
timestampSend date/time. Requires granularity.
marketingActivityIDUnique ID of the campaign or automation workflow
marketingActivityTypeCategory of marketing activityCampaign, Automation
messageIDIn an automation workflow, this refers to a specific message block (also known as block statistics ID)
messageChannelChannel used to send the messageEmail, SMS, Push
segmentIDSegment(s) targeted by the send. Breakdown only - cannot be uses as a filter. A contact can belong to multiple segments; one send counts toward each, so per-segment sums can exceed the overall total. Sends without a segment appear as undefined. Resolve names via GET /api/segments.

Most metrics support every dimension above. Exceptions:

  • totalOrders and totalRevenue only support the timestamp dimension; they cannot be broken down or filtered by marketingActivityID, marketingActivityType, messageID, messageChannel, or segmentID.

Data freshness and availability

Data freshness

Data for the last completed hour is available. The current hour may be incomplete as events are still being processed. Do not rely on partial-hour data for accurate reporting.

Earliest available dates

Metric groupMetricsAvailable from
All metricssent, opened, clicked, attributedRevenue, etc.Up to 5 years from today

Why a query might return empty results

Empty results are not errors. Common reasons:

  • Current hour is incomplete — data for the ongoing hour is not yet finalized.
  • No matching data — no events occurred that match your query's dimensions and filters.

Step-by-step query guide

All examples use POST https://api.omnisend.com/api/analytics/reports with Content-Type: application/json.

Example 1: Total sends per day

The simplest query — one metric, one dimension (timestamp), no filters.

Why: You want to know how many messages your brand sent each day over a few days.

{
  "queries": [
    {
      "alias": "daily-sends",
      "metrics": [{"name": "sent"}],
      "dimensions": [{"name": "timestamp", "granularity": "day"}],
      "dateRange": {
        "interval": "custom",
        "from": "2026-02-01T00:00:00Z",
        "to": "2026-02-04T00:00:00Z"
      }
    }
  ]
}

Response — one row per day:

{
  "reports": [
    {
      "alias": "daily-sends",
      "dimensions": [{"name": "timestamp", "granularity": "day"}],
      "metrics": [{"name": "sent"}],
      "rows": [
        {"timestamp": "2026-02-01", "sent": 1200},
        {"timestamp": "2026-02-02", "sent": 980},
        {"timestamp": "2026-02-03", "sent": 1540}
      ]
    }
  ]
}

Each row contains one key per dimension and one key per metric. The timestamp format depends on granularity — 2026-02-01 for day, 2026-02-01T15:00:00Z for hour.


Example 2: Sends broken down by channel

Add a dimension to split results by email, SMS, and push.

Why: You want to see daily send volume per channel over a couple of days.

{
  "queries": [
    {
      "alias": "sends-by-channel",
      "metrics": [{"name": "sent"}],
      "dimensions": [
        {"name": "timestamp", "granularity": "day"},
        {"name": "messageChannel"}
      ],
      "dateRange": {
        "interval": "custom",
        "from": "2026-02-01T00:00:00Z",
        "to": "2026-02-03T00:00:00Z"
      }
    }
  ]
}

Response — one row per day per channel:

{
  "reports": [
    {
      "alias": "sends-by-channel",
      "dimensions": [
        {"name": "timestamp", "granularity": "day"},
        {"name": "messageChannel"}
      ],
      "metrics": [{"name": "sent"}],
      "rows": [
        {"timestamp": "2026-02-01", "messageChannel": "Email", "sent": 1000},
        {"timestamp": "2026-02-01", "messageChannel": "SMS", "sent": 150},
        {"timestamp": "2026-02-01", "messageChannel": "Push", "sent": 50},
        {"timestamp": "2026-02-02", "messageChannel": "Email", "sent": 1100},
        {"timestamp": "2026-02-02", "messageChannel": "SMS", "sent": 130},
        {"timestamp": "2026-02-02", "messageChannel": "Push", "sent": 45}
      ]
    }
  ]
}

Example 3: Engagement with rate metrics

Use rate metrics alongside absolute counts.

Why: You want to see open and click performance with rates for the last 7 days. Using a named interval means you don't need explicit dates — the range is calculated from the brand's timezone.

{
  "queries": [
    {
      "alias": "engagement-rates",
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"}
      ],
      "dimensions": [{"name": "timestamp", "granularity": "day"}],
      "dateRange": {
        "interval": "last7Days"
      }
    }
  ]
}

Response — one row per day with counts and rates:

{
  "reports": [
    {
      "alias": "engagement-rates",
      "dimensions": [{"name": "timestamp", "granularity": "day"}],
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"}
      ],
      "rows": [
        {"timestamp": "2026-02-10", "sent": 5000, "openedUnique": 1200, "openRate": 0.24, "clickedUnique": 300, "clickRate": 0.06},
        {"timestamp": "2026-02-11", "sent": 4800, "openedUnique": 1100, "openRate": 0.2292, "clickedUnique": 280, "clickRate": 0.0583}
      ]
    }
  ]
}

Example 4: Campaign performance table

Replicate the Campaign Performance table from Omnisend's in-app Reports. The UI shows one row per campaign with aggregated metrics — sent count, engagement rates, revenue, and deliverability indicators. Since timestamp is optional, omitting it returns totals per campaign without time-series breakdown.

Why: You want a per-campaign summary matching the Omnisend Reports → Campaigns view.

{
  "queries": [
    {
      "alias": "campaign-performance",
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"},
        {"name": "attributedOrders"},
        {"name": "attributedOrdersUnique"},
        {"name": "attributedOrderRate"},
        {"name": "attributedRevenue"},
        {"name": "failRate"},
        {"name": "markedAsSpamUnique"},
        {"name": "markedAsSpamRate"},
        {"name": "unsubscribedUnique"},
        {"name": "unsubscribeRate"}
      ],
      "dimensions": [
        {"name": "marketingActivityID"}
      ],
      "dateRange": {
        "interval": "last30Days"
      },
      "filters": [
        {
          "name": "marketingActivityType",
          "operator": "in",
          "values": ["Campaign"]
        }
      ]
    }
  ]
}

Response — one row per campaign:

{
  "reports": [
    {
      "alias": "campaign-performance",
      "dimensions": [
        {"name": "marketingActivityID"}
      ],
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"},
        {"name": "attributedOrders"},
        {"name": "attributedOrdersUnique"},
        {"name": "attributedOrderRate"},
        {"name": "attributedRevenue"},
        {"name": "failRate"},
        {"name": "markedAsSpamUnique"},
        {"name": "markedAsSpamRate"},
        {"name": "unsubscribedUnique"},
        {"name": "unsubscribeRate"}
      ],
      "rows": [
        {
          "marketingActivityID": "c123",
          "sent": 5200,
          "openedUnique": 1248,
          "openRate": 0.24,
          "clickedUnique": 312,
          "clickRate": 0.06,
          "attributedOrders": 78,
          "attributedOrdersUnique": 62,
          "attributedOrderRate": 0.012,
          "attributedRevenue": 1540.00,
          "failRate": 0.008,
          "markedAsSpamUnique": 5,
          "markedAsSpamRate": 0.001,
          "unsubscribedUnique": 26,
          "unsubscribeRate": 0.005
        },
        {
          "marketingActivityID": "c456",
          "sent": 3100,
          "openedUnique": 961,
          "openRate": 0.31,
          "clickedUnique": 279,
          "clickRate": 0.09,
          "attributedOrders": 68,
          "attributedOrdersUnique": 56,
          "attributedOrderRate": 0.018,
          "attributedRevenue": 980.50,
          "failRate": 0.005,
          "markedAsSpamUnique": 2,
          "markedAsSpamRate": 0.0008,
          "unsubscribedUnique": 9,
          "unsubscribeRate": 0.003
        }
      ]
    }
  ]
}

To add a daily breakdown, include the timestamp dimension:

"dimensions": [
  {"name": "timestamp", "granularity": "day"},
  {"name": "marketingActivityID"}
]

Filters use in (include) or notIn (exclude) operators. Multiple filters are combined with AND logic. A filter dimension must be supported by all metrics in the query.


Example 5: A/B campaign metrics

Compare A/B test variants within a single campaign. Filter by the campaign's marketingActivityID and break down by messageID — each variant is a separate message. This lets you compare engagement and revenue across variants to identify the winner.

Why: You want to see how each A/B variant of a campaign performed side by side.

{
  "queries": [
    {
      "alias": "campaign-performance",
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"},
        {"name": "attributedOrdersUnique"},
        {"name": "attributedOrderRate"},
        {"name": "attributedOrders"},
        {"name": "attributedRevenue"},
        {"name": "failRate"},
        {"name": "markedAsSpamUnique"},
        {"name": "markedAsSpamRate"},
        {"name": "unsubscribedUnique"},
        {"name": "unsubscribeRate"}
      ],
      "dimensions": [
        {"name": "messageID"}
      ],
      "dateRange": {
        "interval": "custom",
        "from": "2026-01-01T00:00:00.000Z",
        "to": "2026-02-23T00:00:00.000Z"
      },
      "filters": [
        {
          "name": "marketingActivityType",
          "operator": "in",
          "values": ["Campaign"]
        },
        {
          "name": "marketingActivityID",
          "operator": "in",
          "values": ["697232d1c8ddc672b411bf24"]
        }
      ]
    }
  ]
}

Response — one row per A/B variant (message):

{
  "reports": [
    {
      "alias": "campaign-performance",
      "dimensions": [
        {"name": "messageID"}
      ],
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"},
        {"name": "attributedOrdersUnique"},
        {"name": "attributedOrderRate"},
        {"name": "attributedOrders"},
        {"name": "attributedRevenue"},
        {"name": "failRate"},
        {"name": "markedAsSpamUnique"},
        {"name": "markedAsSpamRate"},
        {"name": "unsubscribedUnique"},
        {"name": "unsubscribeRate"}
      ],
      "rows": [
        {
          "messageID": "msg-variant-a",
          "sent": 5000,
          "openedUnique": 1300,
          "openRate": 0.26,
          "clickedUnique": 350,
          "clickRate": 0.07,
          "attributedOrdersUnique": 40,
          "attributedOrderRate": 0.008,
          "attributedOrders": 45,
          "attributedRevenue": 3825.50,
          "failRate": 0.02,
          "markedAsSpamUnique": 3,
          "markedAsSpamRate": 0.0006,
          "unsubscribedUnique": 15,
          "unsubscribeRate": 0.003
        },
        {
          "messageID": "msg-variant-b",
          "sent": 5000,
          "openedUnique": 1700,
          "openRate": 0.34,
          "clickedUnique": 500,
          "clickRate": 0.10,
          "attributedOrdersUnique": 65,
          "attributedOrderRate": 0.013,
          "attributedOrders": 72,
          "attributedRevenue": 6120.80,
          "failRate": 0.018,
          "markedAsSpamUnique": 2,
          "markedAsSpamRate": 0.0004,
          "unsubscribedUnique": 12,
          "unsubscribeRate": 0.0024
        }
      ]
    }
  ]
}

Example 6: Workflow performance table

Replicate the Workflow Performance table from Omnisend's in-app Reports → Automation view. The UI shows one row per automation workflow with the same metrics as the Campaign Performance table — sent count, engagement rates, revenue, and deliverability indicators. Filter by marketingActivityType: "Automation" and break down by marketingActivityID.

Why: You want a per-automation workflow summary matching the Omnisend Reports → Automation → Workflow Performance table.

{
  "queries": [
    {
      "alias": "workflow-performance",
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"},
        {"name": "attributedOrders"},
        {"name": "attributedOrdersUnique"},
        {"name": "attributedOrderRate"},
        {"name": "attributedRevenue"},
        {"name": "failRate"},
        {"name": "markedAsSpamUnique"},
        {"name": "markedAsSpamRate"},
        {"name": "unsubscribedUnique"},
        {"name": "unsubscribeRate"}
      ],
      "dimensions": [
        {"name": "marketingActivityID"}
      ],
      "dateRange": {
        "interval": "last30Days"
      },
      "filters": [
        {
          "name": "marketingActivityType",
          "operator": "in",
          "values": ["Automation"]
        }
      ]
    }
  ]
}

Response — one row per automation workflow:

{
  "reports": [
    {
      "alias": "workflow-performance",
      "dimensions": [
        {"name": "marketingActivityID"}
      ],
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"},
        {"name": "clickedUnique"},
        {"name": "clickRate"},
        {"name": "attributedOrders"},
        {"name": "attributedOrdersUnique"},
        {"name": "attributedOrderRate"},
        {"name": "attributedRevenue"},
        {"name": "failRate"},
        {"name": "markedAsSpamUnique"},
        {"name": "markedAsSpamRate"},
        {"name": "unsubscribedUnique"},
        {"name": "unsubscribeRate"}
      ],
      "rows": [
        {
          "marketingActivityID": "a101",
          "sent": 8400,
          "openedUnique": 3024,
          "openRate": 0.36,
          "clickedUnique": 672,
          "clickRate": 0.08,
          "attributedOrders": 210,
          "attributedOrdersUnique": 185,
          "attributedOrderRate": 0.022,
          "attributedRevenue": 4250.00,
          "failRate": 0.006,
          "markedAsSpamUnique": 8,
          "markedAsSpamRate": 0.001,
          "unsubscribedUnique": 42,
          "unsubscribeRate": 0.005
        },
        {
          "marketingActivityID": "a202",
          "sent": 2600,
          "openedUnique": 1040,
          "openRate": 0.40,
          "clickedUnique": 338,
          "clickRate": 0.13,
          "attributedOrders": 104,
          "attributedOrdersUnique": 91,
          "attributedOrderRate": 0.035,
          "attributedRevenue": 1870.25,
          "failRate": 0.004,
          "markedAsSpamUnique": 1,
          "markedAsSpamRate": 0.0004,
          "unsubscribedUnique": 7,
          "unsubscribeRate": 0.003
        }
      ]
    }
  ]
}

To drill into individual messages within an automation workflow, add the messageID dimension:

"dimensions": [
  {"name": "marketingActivityID"},
  {"name": "messageID"}
]

This returns one row per message block per automation workflow — useful for identifying which step in an automation workflow performs best.


Example 7: Multiple queries in one request

You can send up to 4 queries in a single request. Each query has its own alias, metrics, dimensions, and filters.

Why: You want campaign metrics and automation workflow metrics in one call, avoiding two separate requests.

{
  "queries": [
    {
      "alias": "campaign_metrics",
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "clickedUnique"},
        {"name": "attributedRevenue"}
      ],
      "dimensions": [
        {"name": "timestamp", "granularity": "day"},
        {"name": "marketingActivityID"}
      ],
      "dateRange": {
        "interval": "lastMonth"
      },
      "filters": [
        {"name": "marketingActivityType", "operator": "in", "values": ["Campaign"]}
      ]
    },
    {
      "alias": "automation_metrics",
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "clickedUnique"},
        {"name": "attributedRevenue"}
      ],
      "dimensions": [
        {"name": "timestamp", "granularity": "day"},
        {"name": "marketingActivityID"}
      ],
      "dateRange": {
        "interval": "lastMonth"
      },
      "filters": [
        {"name": "marketingActivityType", "operator": "in", "values": ["Automation"]}
      ]
    }
  ]
}

Response — one entry in reports per alias. Use the alias to match results to your queries:

{
  "reports": [
    {
      "alias": "campaign_metrics",
      "dimensions": [
        {"name": "timestamp", "granularity": "day"},
        {"name": "marketingActivityID"}
      ],
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "clickedUnique"},
        {"name": "attributedRevenue"}
      ],
      "rows": [
        {
          "timestamp": "2026-01-15",
          "marketingActivityID": "c123",
          "sent": 120,
          "openedUnique": 35,
          "clickedUnique": 10,
          "attributedRevenue": 245.50
        }
      ]
    },
    {
      "alias": "automation_metrics",
      "dimensions": [
        {"name": "timestamp", "granularity": "day"},
        {"name": "marketingActivityID"}
      ],
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "clickedUnique"},
        {"name": "attributedRevenue"}
      ],
      "rows": [
        {
          "timestamp": "2026-01-15",
          "marketingActivityID": "a789",
          "sent": 45,
          "openedUnique": 22,
          "clickedUnique": 9,
          "attributedRevenue": 180.00
        }
      ]
    }
  ]
}

Cross-query constraint: All queries using the custom interval must have date ranges that fall within the same 1 calendar year.


Example 8: Campaign performance broken down by segment

Break down sends and engagement by which audience segment received the campaign. A send targeting multiple segments contributes to each segment's row, so per-segment totals can exceed the overall total. Sends without any segment targeting appear as undefined.

Why: You want to see which segments had the best open rates for campaigns sent in the last 30 days.

{
  "queries": [
    {
      "alias": "by-segment",
      "metrics": [
        {"name": "sent"},
        {"name": "openedUnique"},
        {"name": "openRate"}
      ],
      "dimensions": [
        {"name": "segmentID"}
      ],
      "dateRange": {
        "interval": "last30Days"
      },
      "filters": [
        {
          "name": "marketingActivityType",
          "operator": "in",
          "values": ["Campaign"]
        }
      ]
    }
  ]
}

Response — one row per segment:

{
  "reports": [
    {
      "alias": "by-segment",
      "dimensions": [{"name": "segmentID"}],
      "metrics": [{"name": "sent"}, {"name": "openedUnique"}, {"name": "openRate"}],
      "rows": [
        {"segmentID": "64a1c2d3e4f5a6b7c8d9e0f1", "sent": 5200, "openedUnique": 1560, "openRate": 0.3},
        {"segmentID": "74b2d3e4f5a6b7c8d9e0f1a2", "sent": 3100, "openedUnique": 868, "openRate": 0.28},
        {"segmentID": "undefined", "sent": 400, "openedUnique": 80, "openRate": 0.2}
      ]
    }
  ]
}

Resolve segment IDs to human-readable names by calling GET /api/segments. The undefined row represents sends that had no segment IDs at send time (e.g. manual sends without a segment target).

Note: segmentID is not supported by totalOrders or totalRevenue.


Constraints and limits

All limits are enforced per request and per brand.

Rate limits

Time windowLimit
Per minute10 requests
Per day55 requests

When exceeded, the API returns HTTP 429 Too Many Requests.

Request limits

ConstraintLimit
Queries per request4
Non-timestamp dimensions per query2
Filters per query100
Date span per query (custom interval)1 calendar year
Lookback period (custom interval)5 years from today
Cross-query date range (custom interval)All queries with custom interval must fall within same 1 calendar year

Date range limits by granularity (custom interval only)

GranularityMax date range
hour7 days
day60 days
week52 weeks
month12 months

These limits apply only when using the custom interval with explicit from/to dates. Named intervals have pre-defined ranges that are always valid.

If you need more than 60 days of daily data with a custom interval, split into multiple requests.

Example: invalid request and error

Requesting hourly data for 30 days (exceeds the 7-day limit):

{
  "queries": [
    {
      "alias": "too-wide",
      "metrics": [{"name": "sent"}],
      "dimensions": [{"name": "timestamp", "granularity": "hour"}],
      "dateRange": {
        "interval": "custom",
        "from": "2026-01-01T00:00:00Z",
        "to": "2026-01-31T00:00:00Z"
      }
    }
  ]
}

Response (HTTP 400):

{
  "error": "1 error(s) found. Check 'fields' array for details.",
  "fields": {
    "queries[0].dimensions": "hourly granularity cannot be used with date range greater than 7 days"
  }
}

Agency guidance

If you manage multiple brands (e.g., as an agency), keep the following in mind:

  • One brand per request — each API call is scoped to a single brand. To report across brands, make separate requests per brand.
  • Rate limits are per brand — each brand has its own rate limit counters (10/min, 55/day). Querying one brand does not consume another brand's quota.
  • Authentication — use OAuth or API key authentication.

Technical reference

Endpoint

POST https://api.omnisend.com/api/analytics/reports

Content type: application/json

Request schema

{
  "queries": [
    {
      "alias": "string",
      "metrics": [
        {"name": "string"}
      ],
      "dimensions": [
        {"name": "string", "granularity": "string"}
      ],
      "dateRange": {
        "interval": "string",
        "from": "RFC3339 datetime",
        "to": "RFC3339 datetime"
      },
      "filters": [
        {
          "name": "string",
          "operator": "in | notIn",
          "values": ["string"]
        }
      ]
    }
  ]
}
FieldTypeRequiredDescription
queriesarrayYes1–4 queries per request
queries[].aliasstringYesUnique identifier for the query. Pattern: ^[a-zA-Z0-9 |_.-]+$
queries[].metricsarrayYesAt least 1 metric. No duplicates within a query.
queries[].dimensionsarrayNoOptional. Omitting returns a single totals row. Up to 2 non-timestamp dimensions. If timestamp is included, it requires granularity.
queries[].dimensions[].granularitystringRequired when dimension is timestampOne of: hour, day, week, month
queries[].dateRange.intervalstringYesDate range interval. See Intervals.
queries[].dateRange.fromdatetimeRequired for customRFC3339/ISO8601. Inclusive. Must not be earlier than 5 years from today.
queries[].dateRange.todatetimeRequired for customRFC3339/ISO8601. Exclusive. Must be on or after from.
queries[].filtersarrayNoUp to 100 filters per query. Filter dimension must be supported by all metrics in the query.
queries[].filters[].operatorstringYesin or notIn
queries[].filters[].valuesarrayYesNon-empty array of string values

Response schema

{
  "reports": [
    {
      "alias": "string",
      "dimensions": [
        {"name": "string", "granularity": "string"}
      ],
      "metrics": [
        {"name": "string"}
      ],
      "rows": [
        {
          "dimensionName": "dimensionValue",
          "metricName": 0
        }
      ]
    }
  ]
}
FieldTypeDescription
reportsarrayOne entry per query alias
reports[].aliasstringMatches the alias from the request
reports[].dimensionsarrayEchoes requested dimensions
reports[].metricsarrayEchoes requested metrics
reports[].rowsarrayResult rows. Each row contains one key per dimension and one key per metric.

Metric values are numbers. Dimension values are strings (except timestamp, whose format depends on granularity).

Dimension reference

Dimensions are optional — omitting them returns a single totals row with no breakdown. You can include a timestamp dimension (requires granularity) plus up to 2 non-timestamp dimensions per query.

DimensionDescriptionAvailable values
timestampSend date/time. Requires granularity.
marketingActivityIDUnique ID of the campaign or automation workflow
marketingActivityTypeCategory of marketing activityCampaign, Automation
messageIDIn an automation workflow, this refers to a specific message block (also known as block statistics ID)
messageChannelChannel used to send the messageEmail, SMS, Push

Dimension/Metric compatibility

Most metrics support all non-timestamp dimensions (marketingActivityID, marketingActivityType, messageID, messageChannel). The timestamp dimension is always supported.
totalOrders and totalRevenue are exceptions: they support only the timestamp dimension and cannot be filtered or broken down by non-timestamp dimensions.

Filters

Filters restrict results based on dimension values.

PropertyRequiredDescription
nameYesDimension name to filter on
operatorYesin — include matching values; notIn — exclude matching values
valuesYesNon-empty array of string values to match
  • Maximum 100 filters per query.
  • Multiple filters use AND logic — all conditions must match.

Intervals

The interval field determines how the date range is resolved. It is required on every query.

IntervalDescription
customUse explicit from and to dates (both required)
last7DaysLast 7 days including today
last30DaysLast 30 days including today
last90DaysLast 90 days including today
thisWeekMonday of the current week through now
lastWeekPrevious Monday through Sunday
thisMonthFirst day of the current month through now
lastMonthFirst through last day of the previous month
thisYearJanuary 1st of the current year through now
lastYearJanuary 1st through December 31st of the previous year

For all intervals except custom, the date range is calculated relative to the current time in the brand's timezone. The from and to fields must not be provided for non-custom intervals — doing so returns a validation error.

Allowed granularity by interval

Not every granularity is available for every interval.

IntervalAllowed
customhour, day, week, month (subject to date range limits)
last7Dayshour, day, week, month
last30Daysday, week, month
last90Daysweek, month
thisWeekhour, day, week, month
lastWeekhour, day, week, month
thisMonthday, week, month
lastMonthday, week, month
thisYearweek, month
lastYearweek, month

Date range and timezone

When using the custom interval, both from and to are required in RFC3339/ISO8601 format. The from date must be before or equal to to. The from date is inclusive and to is exclusive.

  • With timezone offset: 2026-02-01T00:00:00+02:00
  • Explicit UTC: 2026-02-01T00:00:00Z

The timezone offset determines how data is bucketed into time periods. If using timezone offsets, both from and to should use the same offset — mixing offsets may produce unexpected results. Brand timezone can be retrieved from Brands API.

2026-02-01T00:00:00+02:00 represents midnight in UTC+2, which is 10:00 PM the previous day in UTC. These mark different points in time and return different data.

For named intervals (last7Days, thisMonth, etc.), the date range is automatically calculated using the brand's timezone.

Error responses

All non-validation errors return {"error": "<message>"}.

Retryable errors

Transient failures are expected. Your integration must retry requests that fail with these status codes.

StatusMeaningRetry
429Too Many Requests — rate limit exceededBack off, then retry
500Internal Server ErrorRetry with exponential backoff
503Service UnavailableRetry with exponential backoff
524Timeout — request timed outRetry with exponential backoff

Socket timeouts and TCP disconnects should also be retried.

Use exponential backoff with large intervals (e.g. 30 s, 120 s, 480 s) between retries.

Non-retryable errors

These indicate a problem with your request. Fix the request before sending again.

StatusMeaning
403Forbidden — invalid brand or insufficient permissions
415Unsupported Media Type — missing or wrong Content-Type

Request validation errors (400)

Returned when the request body fails validation. The fields object maps each invalid field path to an error message.

{
  "error": "1 error(s) found. Check 'fields' array for details.",
  "fields": {
    "queries[0].metrics": "must provide at least one metric"
  }
}

Non-validation 400 errors (e.g. "invalid request body") return {"error": "<message>"} without the fields object.