#Saved charts
GET /saved-charts
List the organization's saved charts across every dashboard. Filter by dashboardId to read one dashboard's charts, or use the dashboard's detail endpoint to get them in reading order.
Requires the analytics:read permission.
Query Parameters
limitnumberThe number of records to return.
cursorstringA pagination cursor. Fetch the next page by passing the nextCursor value from the previous response.
sortname | -name | createdAt | -createdAtThe sort order. Prefix a field with - to sort descending. Defaults to -createdAt.
filterstringA JSON-encoded filter document, for example {"createdAt":{"$gte":"2025-01-01T00:00:00Z"}}. Filterable fields: id, dashboardId, name, resource, chartType, createdAt. See Filtering for the syntax.
curl \
"https://api.cascade.dev/saved-charts" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { getSavedCharts } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await getSavedCharts()require "net/http"
uri = URI("https://api.cascade.dev/saved-charts")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
req = Request(
"https://api.cascade.dev/saved-charts",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": "...",
"groupBy": "...",
"granularity": "...",
"filter": "...",
"limit": "..."
},
"layout": {
"x": "...",
"y": "...",
"w": "...",
"h": "..."
},
"invalidReason": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z"
}
],
"nextCursor": "string"
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Unique identifier for the saved chart."
},
"dashboardId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The dashboard the chart belongs to, for its whole life."
},
"name": {
"type": "string",
"description": "The title shown above the chart."
},
"resource": {
"type": "string",
"enum": [
"reviews",
"review-answers",
"orders",
"customers",
"products",
"loyalty-members",
"loyalty-activity",
"loyalty-rewards"
],
"description": "The resource to aggregate. Call `/analytics/schema` for what each one offers."
},
"chartType": {
"type": "string",
"enum": ["stat", "line", "area", "bar", "donut", "stackedBar", "groupedBar", "table"],
"description": "How the chart is drawn. It has to suit the query it is saved with: a `donut` needs one counting measure grouped by one non-date dimension, a `stat` needs no grouping at all, and `table` always fits."
},
"query": {
"type": "object",
"properties": {
"measures": {
"minItems": 1,
"type": "array",
"items": {
"type": "string"
},
"description": "The measures to report, by name. They have to be ones the resource offers."
},
"groupBy": {
"description": "The dimensions to group by, by name, at most three and at most one of them a date. Omit for a single total row.",
"type": "array",
"items": {
"type": "string"
}
},
"granularity": {
"type": "string",
"enum": ["hour", "day", "week", "month", "quarter", "year"],
"description": "The bucket size for a date dimension. Defaults to `day`. Ignored when nothing in `groupBy` is a date."
},
"filter": {
"description": "The filter document to apply, in the shape the resource's list endpoint takes. It is validated against the fields that screen allows.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"limit": {
"description": "How many distinct group-by combinations to return, from 1 to 500.",
"type": "integer",
"minimum": 1,
"maximum": 500
}
},
"required": ["measures"],
"additionalProperties": false,
"description": "What the chart asks for. Send it to `/analytics/query` alongside `resource` to draw it."
},
"layout": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The column the chart starts at, on a 12 column grid."
},
"y": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The row the chart starts at."
},
"w": {
"type": "integer",
"minimum": 2,
"maximum": 12,
"description": "How many columns the chart spans, at least 2."
},
"h": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"description": "How many rows tall the chart is. One row renders as 90 pixels."
}
},
"required": ["x", "y", "w", "h"],
"additionalProperties": false,
"description": "Where the chart sits on the dashboard's grid."
},
"invalidReason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Why the chart can no longer run, or null when it is fine. Definitions are validated on write, so this only fills in when a resource, measure or dimension the chart names was removed afterward."
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was saved."
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was last changed."
}
},
"required": [
"id",
"dashboardId",
"name",
"resource",
"chartType",
"query",
"layout",
"invalidReason",
"createdAt",
"updatedAt"
],
"additionalProperties": false
},
"description": "The page of records, in the requested sort order."
},
"nextCursor": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Pass this back as `cursor` to fetch the next page. Null once the last page has been returned."
}
},
"required": ["data", "nextCursor"],
"additionalProperties": false
}POST /saved-charts
Save a chart onto a dashboard. The query document is validated against what the resource can measure and group by, its filter against the fields that resource's list endpoint allows, and the chart type against the shape the query returns, so a chart that would error every time it is opened is rejected here instead. Omit layout to append the chart at the bottom of the dashboard.
Requires the analytics:write permission.
Body Parameters
dataobjectrequiredThe chart to save.
curl \
-X POST \
"https://api.cascade.dev/saved-charts" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
},
"layout": {
"x": 0,
"y": 0,
"w": 0,
"h": 0
}
}
}'import { client } from "@cascade-commerce/api/admin/client"
import { postSavedCharts } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await postSavedCharts({
body: {
data: {
dashboardId: "550e8400-e29b-41d4-a716-446655440000",
name: "string",
resource: "reviews",
chartType: "stat",
query: {
measures: ["..."],
groupBy: ["..."],
granularity: "hour",
filter: "...",
limit: 0,
},
layout: {
x: 0,
y: 0,
w: 0,
h: 0,
},
},
},
})require "net/http"
uri = URI("https://api.cascade.dev/saved-charts")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"data": {
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
},
"layout": {
"x": 0,
"y": 0,
"w": 0,
"h": 0
}
}
}
JSON
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
body = """{
"data": {
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
},
"layout": {
"x": 0,
"y": 0,
"w": 0,
"h": 0
}
}
}"""
req = Request(
"https://api.cascade.dev/saved-charts",
data=body.encode(),
headers={
"Authorization": "Bearer sk_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
)
with urlopen(req) as res:
print(json.load(res)){
"id": "550e8400-e29b-41d4-a716-446655440000",
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": ["string"],
"groupBy": ["string"],
"granularity": "hour",
"filter": "...",
"limit": 0
},
"layout": {
"x": 0,
"y": 0,
"w": 0,
"h": 0
},
"invalidReason": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z"
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Unique identifier for the saved chart."
},
"dashboardId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The dashboard the chart belongs to, for its whole life."
},
"name": {
"type": "string",
"description": "The title shown above the chart."
},
"resource": {
"type": "string",
"enum": [
"reviews",
"review-answers",
"orders",
"customers",
"products",
"loyalty-members",
"loyalty-activity",
"loyalty-rewards"
],
"description": "The resource to aggregate. Call `/analytics/schema` for what each one offers."
},
"chartType": {
"type": "string",
"enum": ["stat", "line", "area", "bar", "donut", "stackedBar", "groupedBar", "table"],
"description": "How the chart is drawn. It has to suit the query it is saved with: a `donut` needs one counting measure grouped by one non-date dimension, a `stat` needs no grouping at all, and `table` always fits."
},
"query": {
"type": "object",
"properties": {
"measures": {
"minItems": 1,
"type": "array",
"items": {
"type": "string"
},
"description": "The measures to report, by name. They have to be ones the resource offers."
},
"groupBy": {
"description": "The dimensions to group by, by name, at most three and at most one of them a date. Omit for a single total row.",
"type": "array",
"items": {
"type": "string"
}
},
"granularity": {
"type": "string",
"enum": ["hour", "day", "week", "month", "quarter", "year"],
"description": "The bucket size for a date dimension. Defaults to `day`. Ignored when nothing in `groupBy` is a date."
},
"filter": {
"description": "The filter document to apply, in the shape the resource's list endpoint takes. It is validated against the fields that screen allows.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"limit": {
"description": "How many distinct group-by combinations to return, from 1 to 500.",
"type": "integer",
"minimum": 1,
"maximum": 500
}
},
"required": ["measures"],
"additionalProperties": false,
"description": "What the chart asks for. Send it to `/analytics/query` alongside `resource` to draw it."
},
"layout": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The column the chart starts at, on a 12 column grid."
},
"y": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The row the chart starts at."
},
"w": {
"type": "integer",
"minimum": 2,
"maximum": 12,
"description": "How many columns the chart spans, at least 2."
},
"h": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"description": "How many rows tall the chart is. One row renders as 90 pixels."
}
},
"required": ["x", "y", "w", "h"],
"additionalProperties": false,
"description": "Where the chart sits on the dashboard's grid."
},
"invalidReason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Why the chart can no longer run, or null when it is fine. Definitions are validated on write, so this only fills in when a resource, measure or dimension the chart names was removed afterward."
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was saved."
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was last changed."
}
},
"required": [
"id",
"dashboardId",
"name",
"resource",
"chartType",
"query",
"layout",
"invalidReason",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
"description": "The new saved chart."
}POST /saved-charts/preview
Check a chart definition without saving it. It runs exactly the validation POST /saved-charts runs, and reports which chart types can draw the query, so a type that does not fit is caught before anything is written. Nothing is stored and nothing is recorded.
Requires the analytics:read permission.
Body Parameters
dataobjectrequiredThe chart definition to check.
curl \
-X POST \
"https://api.cascade.dev/saved-charts/preview" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
}
}
}'import { client } from "@cascade-commerce/api/admin/client"
import { postSavedChartsPreview } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await postSavedChartsPreview({
body: {
data: {
resource: "reviews",
chartType: "stat",
query: {
measures: ["..."],
groupBy: ["..."],
granularity: "hour",
filter: "...",
limit: 0,
},
},
},
})require "net/http"
uri = URI("https://api.cascade.dev/saved-charts/preview")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"data": {
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
}
}
}
JSON
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
body = """{
"data": {
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
}
}
}"""
req = Request(
"https://api.cascade.dev/saved-charts/preview",
data=body.encode(),
headers={
"Authorization": "Bearer sk_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
)
with urlopen(req) as res:
print(json.load(res)){
"valid": true,
"reason": "string",
"chartTypes": ["stat"],
"recommendedChartType": "stat"
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"valid": {
"type": "boolean",
"description": "Whether this definition would be accepted by `POST /saved-charts` as it stands."
},
"reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Why it would be refused, in the words the save would use, or null when it is fine."
},
"chartTypes": {
"type": "array",
"items": {
"type": "string",
"enum": ["stat", "line", "area", "bar", "donut", "stackedBar", "groupedBar", "table"],
"description": "How the chart is drawn. It has to suit the query it is saved with: a `donut` needs one counting measure grouped by one non-date dimension, a `stat` needs no grouping at all, and `table` always fits."
},
"description": "Every chart type that can draw this query, best first. Empty when the query itself is the problem, since a query that cannot run has no shape to draw."
},
"recommendedChartType": {
"anyOf": [
{
"type": "string",
"enum": ["stat", "line", "area", "bar", "donut", "stackedBar", "groupedBar", "table"],
"description": "How the chart is drawn. It has to suit the query it is saved with: a `donut` needs one counting measure grouped by one non-date dimension, a `stat` needs no grouping at all, and `table` always fits."
},
{
"type": "null"
}
],
"description": "The chart type that suits this query best, or null when the query cannot run."
}
},
"required": ["valid", "reason", "chartTypes", "recommendedChartType"],
"additionalProperties": false,
"description": "What the save would make of this definition."
}GET /saved-charts/{id}
Get a single saved chart by ID.
Requires the analytics:read permission.
Path Parameters
idstring (uuid)requiredThe ID of the saved chart.
curl \
"https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { getSavedChartsById } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await getSavedChartsById({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
req = Request(
"https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"id": "550e8400-e29b-41d4-a716-446655440000",
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": ["string"],
"groupBy": ["string"],
"granularity": "hour",
"filter": "...",
"limit": 0
},
"layout": {
"x": 0,
"y": 0,
"w": 0,
"h": 0
},
"invalidReason": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z"
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Unique identifier for the saved chart."
},
"dashboardId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The dashboard the chart belongs to, for its whole life."
},
"name": {
"type": "string",
"description": "The title shown above the chart."
},
"resource": {
"type": "string",
"enum": [
"reviews",
"review-answers",
"orders",
"customers",
"products",
"loyalty-members",
"loyalty-activity",
"loyalty-rewards"
],
"description": "The resource to aggregate. Call `/analytics/schema` for what each one offers."
},
"chartType": {
"type": "string",
"enum": ["stat", "line", "area", "bar", "donut", "stackedBar", "groupedBar", "table"],
"description": "How the chart is drawn. It has to suit the query it is saved with: a `donut` needs one counting measure grouped by one non-date dimension, a `stat` needs no grouping at all, and `table` always fits."
},
"query": {
"type": "object",
"properties": {
"measures": {
"minItems": 1,
"type": "array",
"items": {
"type": "string"
},
"description": "The measures to report, by name. They have to be ones the resource offers."
},
"groupBy": {
"description": "The dimensions to group by, by name, at most three and at most one of them a date. Omit for a single total row.",
"type": "array",
"items": {
"type": "string"
}
},
"granularity": {
"type": "string",
"enum": ["hour", "day", "week", "month", "quarter", "year"],
"description": "The bucket size for a date dimension. Defaults to `day`. Ignored when nothing in `groupBy` is a date."
},
"filter": {
"description": "The filter document to apply, in the shape the resource's list endpoint takes. It is validated against the fields that screen allows.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"limit": {
"description": "How many distinct group-by combinations to return, from 1 to 500.",
"type": "integer",
"minimum": 1,
"maximum": 500
}
},
"required": ["measures"],
"additionalProperties": false,
"description": "What the chart asks for. Send it to `/analytics/query` alongside `resource` to draw it."
},
"layout": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The column the chart starts at, on a 12 column grid."
},
"y": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The row the chart starts at."
},
"w": {
"type": "integer",
"minimum": 2,
"maximum": 12,
"description": "How many columns the chart spans, at least 2."
},
"h": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"description": "How many rows tall the chart is. One row renders as 90 pixels."
}
},
"required": ["x", "y", "w", "h"],
"additionalProperties": false,
"description": "Where the chart sits on the dashboard's grid."
},
"invalidReason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Why the chart can no longer run, or null when it is fine. Definitions are validated on write, so this only fills in when a resource, measure or dimension the chart names was removed afterward."
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was saved."
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was last changed."
}
},
"required": [
"id",
"dashboardId",
"name",
"resource",
"chartType",
"query",
"layout",
"invalidReason",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
"description": "The requested saved chart."
}PUT /saved-charts/{id}
Rename a saved chart, change how it is drawn, or replace what it asks for. The resource it aggregates and the dashboard it belongs to cannot be changed, and its position is written through the dashboard's layout endpoint. The whole definition is revalidated, so a new query the saved chart type cannot draw is rejected.
Requires the analytics:write permission.
Path Parameters
idstring (uuid)requiredThe ID of the saved chart to update.
Body Parameters
dataobjectrequiredThe fields to update.
curl \
-X PUT \
"https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"name": "string",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
}
}
}'import { client } from "@cascade-commerce/api/admin/client"
import { putSavedChartsById } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await putSavedChartsById({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
body: {
data: {
name: "string",
chartType: "stat",
query: {
measures: ["..."],
groupBy: ["..."],
granularity: "hour",
filter: "...",
limit: 0,
},
},
},
})require "net/http"
uri = URI("https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"data": {
"name": "string",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
}
}
}
JSON
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
body = """{
"data": {
"name": "string",
"chartType": "stat",
"query": {
"measures": [
"..."
],
"groupBy": [
"..."
],
"granularity": "hour",
"filter": "...",
"limit": 0
}
}
}"""
req = Request(
"https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000",
data=body.encode(),
method="PUT",
headers={
"Authorization": "Bearer sk_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
)
with urlopen(req) as res:
print(json.load(res)){
"id": "550e8400-e29b-41d4-a716-446655440000",
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": ["string"],
"groupBy": ["string"],
"granularity": "hour",
"filter": "...",
"limit": 0
},
"layout": {
"x": 0,
"y": 0,
"w": 0,
"h": 0
},
"invalidReason": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z"
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Unique identifier for the saved chart."
},
"dashboardId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The dashboard the chart belongs to, for its whole life."
},
"name": {
"type": "string",
"description": "The title shown above the chart."
},
"resource": {
"type": "string",
"enum": [
"reviews",
"review-answers",
"orders",
"customers",
"products",
"loyalty-members",
"loyalty-activity",
"loyalty-rewards"
],
"description": "The resource to aggregate. Call `/analytics/schema` for what each one offers."
},
"chartType": {
"type": "string",
"enum": ["stat", "line", "area", "bar", "donut", "stackedBar", "groupedBar", "table"],
"description": "How the chart is drawn. It has to suit the query it is saved with: a `donut` needs one counting measure grouped by one non-date dimension, a `stat` needs no grouping at all, and `table` always fits."
},
"query": {
"type": "object",
"properties": {
"measures": {
"minItems": 1,
"type": "array",
"items": {
"type": "string"
},
"description": "The measures to report, by name. They have to be ones the resource offers."
},
"groupBy": {
"description": "The dimensions to group by, by name, at most three and at most one of them a date. Omit for a single total row.",
"type": "array",
"items": {
"type": "string"
}
},
"granularity": {
"type": "string",
"enum": ["hour", "day", "week", "month", "quarter", "year"],
"description": "The bucket size for a date dimension. Defaults to `day`. Ignored when nothing in `groupBy` is a date."
},
"filter": {
"description": "The filter document to apply, in the shape the resource's list endpoint takes. It is validated against the fields that screen allows.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"limit": {
"description": "How many distinct group-by combinations to return, from 1 to 500.",
"type": "integer",
"minimum": 1,
"maximum": 500
}
},
"required": ["measures"],
"additionalProperties": false,
"description": "What the chart asks for. Send it to `/analytics/query` alongside `resource` to draw it."
},
"layout": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The column the chart starts at, on a 12 column grid."
},
"y": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The row the chart starts at."
},
"w": {
"type": "integer",
"minimum": 2,
"maximum": 12,
"description": "How many columns the chart spans, at least 2."
},
"h": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"description": "How many rows tall the chart is. One row renders as 90 pixels."
}
},
"required": ["x", "y", "w", "h"],
"additionalProperties": false,
"description": "Where the chart sits on the dashboard's grid."
},
"invalidReason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Why the chart can no longer run, or null when it is fine. Definitions are validated on write, so this only fills in when a resource, measure or dimension the chart names was removed afterward."
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was saved."
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was last changed."
}
},
"required": [
"id",
"dashboardId",
"name",
"resource",
"chartType",
"query",
"layout",
"invalidReason",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
"description": "The updated saved chart."
}DELETE /saved-charts/{id}
Delete a saved chart from its dashboard. The deletion is recorded in the audit log.
Requires the analytics:write permission.
Path Parameters
idstring (uuid)requiredThe ID of the saved chart to delete.
curl \
-X DELETE \
"https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { deleteSavedChartsById } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await deleteSavedChartsById({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.bodyimport json
from urllib.request import Request, urlopen
req = Request(
"https://api.cascade.dev/saved-charts/550e8400-e29b-41d4-a716-446655440000",
method="DELETE",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"id": "550e8400-e29b-41d4-a716-446655440000",
"dashboardId": "550e8400-e29b-41d4-a716-446655440000",
"name": "string",
"resource": "reviews",
"chartType": "stat",
"query": {
"measures": ["string"],
"groupBy": ["string"],
"granularity": "hour",
"filter": "...",
"limit": 0
},
"layout": {
"x": 0,
"y": 0,
"w": 0,
"h": 0
},
"invalidReason": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z"
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "Unique identifier for the saved chart."
},
"dashboardId": {
"type": "string",
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
"description": "The dashboard the chart belongs to, for its whole life."
},
"name": {
"type": "string",
"description": "The title shown above the chart."
},
"resource": {
"type": "string",
"enum": [
"reviews",
"review-answers",
"orders",
"customers",
"products",
"loyalty-members",
"loyalty-activity",
"loyalty-rewards"
],
"description": "The resource to aggregate. Call `/analytics/schema` for what each one offers."
},
"chartType": {
"type": "string",
"enum": ["stat", "line", "area", "bar", "donut", "stackedBar", "groupedBar", "table"],
"description": "How the chart is drawn. It has to suit the query it is saved with: a `donut` needs one counting measure grouped by one non-date dimension, a `stat` needs no grouping at all, and `table` always fits."
},
"query": {
"type": "object",
"properties": {
"measures": {
"minItems": 1,
"type": "array",
"items": {
"type": "string"
},
"description": "The measures to report, by name. They have to be ones the resource offers."
},
"groupBy": {
"description": "The dimensions to group by, by name, at most three and at most one of them a date. Omit for a single total row.",
"type": "array",
"items": {
"type": "string"
}
},
"granularity": {
"type": "string",
"enum": ["hour", "day", "week", "month", "quarter", "year"],
"description": "The bucket size for a date dimension. Defaults to `day`. Ignored when nothing in `groupBy` is a date."
},
"filter": {
"description": "The filter document to apply, in the shape the resource's list endpoint takes. It is validated against the fields that screen allows.",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"limit": {
"description": "How many distinct group-by combinations to return, from 1 to 500.",
"type": "integer",
"minimum": 1,
"maximum": 500
}
},
"required": ["measures"],
"additionalProperties": false,
"description": "What the chart asks for. Send it to `/analytics/query` alongside `resource` to draw it."
},
"layout": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The column the chart starts at, on a 12 column grid."
},
"y": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "The row the chart starts at."
},
"w": {
"type": "integer",
"minimum": 2,
"maximum": 12,
"description": "How many columns the chart spans, at least 2."
},
"h": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"description": "How many rows tall the chart is. One row renders as 90 pixels."
}
},
"required": ["x", "y", "w", "h"],
"additionalProperties": false,
"description": "Where the chart sits on the dashboard's grid."
},
"invalidReason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Why the chart can no longer run, or null when it is fine. Definitions are validated on write, so this only fills in when a resource, measure or dimension the chart names was removed afterward."
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was saved."
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"description": "When the chart was last changed."
}
},
"required": [
"id",
"dashboardId",
"name",
"resource",
"chartType",
"query",
"layout",
"invalidReason",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
"description": "The deleted saved chart."
}