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 groupedResults 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
| Metric | Description |
|---|---|
sent | Total messages sent |
failed | Messages that failed to deliver (bounces, invalid addresses, etc.) |
failRate | Ratio of failed messages to sent messages |
sentCost | Cost incurred for sending messages. SMS only. |
markedAsSpamUnique | Unique messages marked as spam by recipients |
markedAsSpamRate | Ratio of spam-marked messages to sent messages |
unsubscribedUnique | Unique messages that resulted in an unsubscribe |
unsubscribeRate | Ratio of unsubscribed messages to sent messages |
Engagement
| Metric | Description |
|---|---|
openedUnique | Unique messages opened at least once |
opened | Total opens, including repeat opens of the same message |
openRate | Ratio of unique opens to sent messages |
clickedUnique | Unique messages clicked at least once |
clicked | Total clicks, including repeat clicks on the same message |
clickRate | Ratio of unique clicks to sent messages |
Revenue attribution
| Metric | Description |
|---|---|
attributedOrdersUnique | Unique messages with at least one attributed order |
attributedOrders | Total placed orders attributed to messages |
attributedOrderRate | Ratio of messages with at least one order to sent messages |
attributedRevenue | Total revenue from orders attributed to messages |
attributedRevenuePerOrder | Average revenue per attributed order |
attributedRevenuePerSent | Average revenue per sent message |
Total shop sales (sent-based)
| Metric | Description |
|---|---|
totalOrders | Placed orders from message recipients, counted regardless of attribution classification |
totalRevenue | Revenue 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.
| Dimension | Description | Available values |
|---|---|---|
timestamp | Send date/time. Requires granularity. | — |
marketingActivityID | Unique ID of the campaign or automation workflow | — |
marketingActivityType | Category of marketing activity | Campaign, Automation |
messageID | In an automation workflow, this refers to a specific message block (also known as block statistics ID) | — |
messageChannel | Channel used to send the message | Email, SMS, Push |
segmentID | Segment(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:
totalOrdersandtotalRevenueonly support thetimestampdimension; they cannot be broken down or filtered bymarketingActivityID,marketingActivityType,messageID,messageChannel, orsegmentID.
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 group | Metrics | Available from |
|---|---|---|
| All metrics | sent, 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
custominterval 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:
segmentIDis not supported bytotalOrdersortotalRevenue.
Constraints and limits
All limits are enforced per request and per brand.
Rate limits
| Time window | Limit |
|---|---|
| Per minute | 10 requests |
| Per day | 55 requests |
When exceeded, the API returns HTTP 429 Too Many Requests.
Request limits
| Constraint | Limit |
|---|---|
| Queries per request | 4 |
| Non-timestamp dimensions per query | 2 |
| Filters per query | 100 |
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)
custom interval only)| Granularity | Max date range |
|---|---|
hour | 7 days |
day | 60 days |
week | 52 weeks |
month | 12 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"]
}
]
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
queries | array | Yes | 1–4 queries per request |
queries[].alias | string | Yes | Unique identifier for the query. Pattern: ^[a-zA-Z0-9 |_.-]+$ |
queries[].metrics | array | Yes | At least 1 metric. No duplicates within a query. |
queries[].dimensions | array | No | Optional. Omitting returns a single totals row. Up to 2 non-timestamp dimensions. If timestamp is included, it requires granularity. |
queries[].dimensions[].granularity | string | Required when dimension is timestamp | One of: hour, day, week, month |
queries[].dateRange.interval | string | Yes | Date range interval. See Intervals. |
queries[].dateRange.from | datetime | Required for custom | RFC3339/ISO8601. Inclusive. Must not be earlier than 5 years from today. |
queries[].dateRange.to | datetime | Required for custom | RFC3339/ISO8601. Exclusive. Must be on or after from. |
queries[].filters | array | No | Up to 100 filters per query. Filter dimension must be supported by all metrics in the query. |
queries[].filters[].operator | string | Yes | in or notIn |
queries[].filters[].values | array | Yes | Non-empty array of string values |
Response schema
{
"reports": [
{
"alias": "string",
"dimensions": [
{"name": "string", "granularity": "string"}
],
"metrics": [
{"name": "string"}
],
"rows": [
{
"dimensionName": "dimensionValue",
"metricName": 0
}
]
}
]
}| Field | Type | Description |
|---|---|---|
reports | array | One entry per query alias |
reports[].alias | string | Matches the alias from the request |
reports[].dimensions | array | Echoes requested dimensions |
reports[].metrics | array | Echoes requested metrics |
reports[].rows | array | Result 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.
| Dimension | Description | Available values |
|---|---|---|
timestamp | Send date/time. Requires granularity. | — |
marketingActivityID | Unique ID of the campaign or automation workflow | — |
marketingActivityType | Category of marketing activity | Campaign, Automation |
messageID | In an automation workflow, this refers to a specific message block (also known as block statistics ID) | — |
messageChannel | Channel used to send the message | Email, 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.
| Property | Required | Description |
|---|---|---|
name | Yes | Dimension name to filter on |
operator | Yes | in — include matching values; notIn — exclude matching values |
values | Yes | Non-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.
| Interval | Description |
|---|---|
custom | Use explicit from and to dates (both required) |
last7Days | Last 7 days including today |
last30Days | Last 30 days including today |
last90Days | Last 90 days including today |
thisWeek | Monday of the current week through now |
lastWeek | Previous Monday through Sunday |
thisMonth | First day of the current month through now |
lastMonth | First through last day of the previous month |
thisYear | January 1st of the current year through now |
lastYear | January 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.
| Interval | Allowed |
|---|---|
custom | hour, day, week, month (subject to date range limits) |
last7Days | hour, day, week, month |
last30Days | day, week, month |
last90Days | week, month |
thisWeek | hour, day, week, month |
lastWeek | hour, day, week, month |
thisMonth | day, week, month |
lastMonth | day, week, month |
thisYear | week, month |
lastYear | week, 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.
| Status | Meaning | Retry |
|---|---|---|
| 429 | Too Many Requests — rate limit exceeded | Back off, then retry |
| 500 | Internal Server Error | Retry with exponential backoff |
| 503 | Service Unavailable | Retry with exponential backoff |
| 524 | Timeout — request timed out | Retry 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.
| Status | Meaning |
|---|---|
| 403 | Forbidden — invalid brand or insufficient permissions |
| 415 | Unsupported 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.