Docs·Reports
/api/v1/spreelytics/reportsCreate a report
Saves a report and returns it. A report is a name plus a config: the report builder's own settings, so the app can open it in the builder and draw it on a dashboard. Every report sits on one dashboard, named by dashboardId, and you can pin its place on the grid with layout if you want to. Saving runs nothing; run the report with GET /reports/{reportId}/chart.
The config object:
chartType,intervalandrangeare required.chartTypeis one of the builder's types:line(shown as Linear),bar,histogram,pie,metric,area,map,retention,funnel,conversionorsankey.interval,range,startDate,endDate,customDays,previous,breakdownsandglobalFilterstake whatPOST /charttakes, and a filter row may also carry the builder's ownid.limitis stored but not applied when the report runs.seriesholds the metrics, at most 20. Each needs anid(any string) and anevent.segmentsays how it is measured:event(All events, the default),user(Unique users),session(Unique sessions),user_average(Average users),one_event_per_user(One event per user), orproperty_sum,property_average,property_maxandproperty_min(Sum of property, Average of property, Max of property, Min of property) together with aproperty.typeisevent(the default),profile,cohort(with a segment id incohortId) orformula. A formula series keeps an expression such asA/Binformula, naming the other series by their letters; the app works it out from their results, and the server never runs it.- To count several events as one metric, list them in
eventsas{ id, name }objects, the first one being the series' ownevent. Forfunnel,conversionandretention, this list holds the steps in order, all inside a single series. A filter in a series'filtersthat carries a member'sidineventapplies to that step only. - Options for one chart type:
funnelWindowSeconds(funnel and conversion),funnelStrictOrder(funnel),retentionCriteria(retention),sankeyExcludeplusstepsBeforeandstepsAfteron each series (sankey), andstacked(histogram). kindischartunless you set it. Two other kinds have no series:breakdown, a tabbed list card like the Overview's, whose tabs go intabKeys(channels,sources,referrers,utm_source,utm_medium,utm_campaign,utm_term,utm_content,browsers,os,devices,countries,regions,cities,pages,entry,exit,events), andmetrics, a strip of headline numbers named inmetrics(visitors,revenue,conversion-rate,revenue-per-visitor,bounce-rate,avg-session-duration,cohort-retention,online), whereshowChart: trueadds the chart under them. Each needs at least one entry.
{
"name": "Signups by source",
"dashboardId": "DASHBOARD_ID",
"config": {
"chartType": "bar",
"interval": "day",
"range": "30d",
"series": [{ "id": "a", "event": "signup", "segment": "user" }],
"breakdowns": ["source"]
}
}
That is a bar chart of the people who signed up in the last 30 days, split by source. The server checks the name, that a chart card has 1 to 20 series (a breakdown or metrics card needs at least one tab or number), that startDate comes before endDate when both are set, and that dashboardId is a dashboard in this audience. An unknown field answers 400. Everything else is stored as sent, so a config the engine cannot draw only shows up when you run it: try the chart with POST /chart first.
Needs the Editor or Admin role in the workspace that owns the audience. A Viewer gets 403.
Keys: an account key. Getting started explains both kinds.
Parameters
| Prop | In | Type | Notes |
|---|---|---|---|
contactListId | query | string | The audience id, a UUID. With an audience key it must be that key's own audience. |
Request body
| Prop | Type | Notes |
|---|---|---|
name | string | Required. Trimmed, and a blank name answers 400. |
config | SpreelyticsSavedReportConfig | Required. The report builder's settings, described above. |
dashboardId | string | Required. The dashboard to put it on. It must be a dashboard in this audience, or the call answers 400. |
layout? | ReportLayoutSlot | Optional. Its slot on the dashboard grid as { x, y, w, h }: x and w in columns of a 6-column grid (x from 0), y and h in rows. The app sizes a new card 3 by 2 and draws none smaller than 2 by 2. Leave it out and the app places the card itself. |
Response
| Prop | Type | Notes |
|---|---|---|
id | string | The report id. |
name | string | The report name. |
dashboardId | string | The dashboard it sits on. Every report sits on one. |
config | SpreelyticsSavedReportConfig | The saved settings, exactly as they were sent. POST /reports describes them. |
layout | object | Its slot on the dashboard grid as { x, y, w, h }, or null when the app places it. |
createdAt | string | When it was created. Only the create call answers in ISO 8601 (2026-09-30T09:00:00.000Z). Every other call returns the database form (2026-09-30 09:00:00+00), so parse both. |
updatedAt | string | When it last changed. Create and update calls answer in ISO 8601, lists and reads in the database form, as with createdAt. |
Request
curl -X POST 'https://app.spreelytics.com/api/v1/spreelytics/reports?contactListId=…' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{ … }'
const res = await fetch('https://app.spreelytics.com/api/v1/spreelytics/reports', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ /* see Request body above */ }),
});
const data = await res.json();
import requests
res = requests.post(
"https://app.spreelytics.com/api/v1/spreelytics/reports",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={}, # see Request body above
)
data = res.json()
Response
{
"id": "string",
"name": "string",
"dashboardId": "string",
"config": {
"kind": "chart",
"chartType": "line",
"interval": "minute",
"range": "30min",
"startDate": "string",
"endDate": "string",
"series": [
{
"id": "string",
"type": "event",
"event": "string",
"events": [
null
],
"eventLabels": {},
"stepsBefore": 0,
"stepsAfter": 0,
"displayName": "string",
"segment": "event",
"property": "string",
"cohortId": "string",
"filters": [
null
],
"formula": "string",
"hideSeries": [
null
]
}
],
"breakdowns": [
"string"
],
"breakdown": "string",
"globalFilters": [
{
"id": "string",
"name": "string",
"operator": "is",
"value": [
null
],
"event": "string"
}
],
"previous": true,
"customDays": 0,
"limit": 0,
"funnelWindowSeconds": 0,
"funnelStrictOrder": true,
"retentionCriteria": "on",
"sankeyExclude": [
"string"
],
"stacked": true,
"tabKeys": [
"string"
],
"metrics": [
"string"
],
"showChart": true
},
"layout": "string",
"createdAt": "string",
"updatedAt": "string"
}