#Products
GET /products/{id}/review-summary
Get the generated summary of what this product's reviewers say, along with its themes and what it was generated from. Returns null until the product has enough published reviews for one.
Requires the reviews:read permission.
Path Parameters
idstring (uuid)requiredThe ID of the product.
curl \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { getProductsByIdReviewSummary } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await getProductsByIdReviewSummary({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary")
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/products/550e8400-e29b-41d4-a716-446655440000/review-summary",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"productId": "550e8400-e29b-41d4-a716-446655440000",
"summary": "string",
"themes": [
{
"label": "string",
"sentiment": "positive",
"reviewCount": 0
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z",
"suppressedAt": "2025-01-15T09:30:00Z",
"previous": {
"summary": "string",
"themes": [
{
"label": "...",
"sentiment": "...",
"reviewCount": "..."
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z"
}
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{
"type": "object",
"properties": {
"productId": {
"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 product this summary describes."
},
"summary": {
"type": "string",
"description": "Two or three sentences on what reviewers say, generated from the reviews."
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The language the summary is written in, which is the one most reviews are in."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many published reviews the summary was generated from."
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When the newest review it covers was written."
},
"modelId": {
"type": "string",
"description": "The model that produced it."
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Which version of the instructions produced it."
},
"generatedAt": {
"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 it was generated."
},
"suppressedAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When it was hidden from the storefront, or null if it is showing."
},
"previous": {
"anyOf": [
{
"type": "object",
"properties": {
"summary": {
"type": "string"
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
]
},
"modelId": {
"type": "string"
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"generatedAt": {
"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))$"
}
},
"required": [
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt"
],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The summary this one replaced, kept until it is accepted or reverted to. Null once there is nothing to go back to."
}
},
"required": [
"productId",
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt",
"suppressedAt",
"previous"
],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The product's review summary, or null if it has none."
}PATCH /products/{id}/review-summary
Hide a product's review summary from your storefront, or put it back. A hidden summary is also left alone by the regeneration policy, so hiding it is not undone by the next batch of reviews.
Requires the reviews:write permission.
Path Parameters
idstring (uuid)requiredThe ID of the product.
Body Parameters
suppressedbooleanrequiredTrue hides the summary from your storefront and stops it being regenerated. False puts it back.
curl \
-X PATCH \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"suppressed": true
}'import { client } from "@cascade-commerce/api/admin/client"
import { patchProductsByIdReviewSummary } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await patchProductsByIdReviewSummary({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
body: {
suppressed: true,
},
})require "net/http"
uri = URI("https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"suppressed": true
}
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 = """{
"suppressed": true
}"""
req = Request(
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary",
data=body.encode(),
method="PATCH",
headers={
"Authorization": "Bearer sk_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
)
with urlopen(req) as res:
print(json.load(res)){
"productId": "550e8400-e29b-41d4-a716-446655440000",
"summary": "string",
"themes": [
{
"label": "string",
"sentiment": "positive",
"reviewCount": 0
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z",
"suppressedAt": "2025-01-15T09:30:00Z",
"previous": {
"summary": "string",
"themes": [
{
"label": "...",
"sentiment": "...",
"reviewCount": "..."
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z"
}
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"productId": {
"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 product this summary describes."
},
"summary": {
"type": "string",
"description": "Two or three sentences on what reviewers say, generated from the reviews."
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The language the summary is written in, which is the one most reviews are in."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many published reviews the summary was generated from."
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When the newest review it covers was written."
},
"modelId": {
"type": "string",
"description": "The model that produced it."
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Which version of the instructions produced it."
},
"generatedAt": {
"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 it was generated."
},
"suppressedAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When it was hidden from the storefront, or null if it is showing."
},
"previous": {
"anyOf": [
{
"type": "object",
"properties": {
"summary": {
"type": "string"
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
]
},
"modelId": {
"type": "string"
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"generatedAt": {
"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))$"
}
},
"required": [
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt"
],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The summary this one replaced, kept until it is accepted or reverted to. Null once there is nothing to go back to."
}
},
"required": [
"productId",
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt",
"suppressedAt",
"previous"
],
"additionalProperties": false,
"description": "The updated review summary."
}POST /products/{id}/review-summary/regenerate
Queue a fresh summary for this product. The new one goes live as soon as it is written, and the one it replaces is kept so you can go back to it. A product can be regenerated once a day.
Requires the reviews:write permission.
Costs 5 rate limit tokens instead of the usual one. See Rate limits.
Path Parameters
idstring (uuid)requiredThe ID of the product.
curl \
-X POST \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary/regenerate" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { postProductsByIdReviewSummaryRegenerate } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await postProductsByIdReviewSummaryRegenerate({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary/regenerate")
req = Net::HTTP::Post.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/products/550e8400-e29b-41d4-a716-446655440000/review-summary/regenerate",
method="POST",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"queued": true
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"queued": {
"type": "boolean",
"description": "True once the regeneration has been queued."
}
},
"required": ["queued"],
"additionalProperties": false
}POST /products/{id}/review-summary/accept
Keep the current summary and forget the one it replaced. After this there is nothing to revert to.
Requires the reviews:write permission.
Path Parameters
idstring (uuid)requiredThe ID of the product.
curl \
-X POST \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary/accept" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { postProductsByIdReviewSummaryAccept } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await postProductsByIdReviewSummaryAccept({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary/accept")
req = Net::HTTP::Post.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/products/550e8400-e29b-41d4-a716-446655440000/review-summary/accept",
method="POST",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"productId": "550e8400-e29b-41d4-a716-446655440000",
"summary": "string",
"themes": [
{
"label": "string",
"sentiment": "positive",
"reviewCount": 0
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z",
"suppressedAt": "2025-01-15T09:30:00Z",
"previous": {
"summary": "string",
"themes": [
{
"label": "...",
"sentiment": "...",
"reviewCount": "..."
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z"
}
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"productId": {
"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 product this summary describes."
},
"summary": {
"type": "string",
"description": "Two or three sentences on what reviewers say, generated from the reviews."
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The language the summary is written in, which is the one most reviews are in."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many published reviews the summary was generated from."
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When the newest review it covers was written."
},
"modelId": {
"type": "string",
"description": "The model that produced it."
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Which version of the instructions produced it."
},
"generatedAt": {
"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 it was generated."
},
"suppressedAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When it was hidden from the storefront, or null if it is showing."
},
"previous": {
"anyOf": [
{
"type": "object",
"properties": {
"summary": {
"type": "string"
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
]
},
"modelId": {
"type": "string"
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"generatedAt": {
"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))$"
}
},
"required": [
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt"
],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The summary this one replaced, kept until it is accepted or reverted to. Null once there is nothing to go back to."
}
},
"required": [
"productId",
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt",
"suppressedAt",
"previous"
],
"additionalProperties": false,
"description": "The accepted review summary."
}POST /products/{id}/review-summary/revert
Put the previous summary back and discard the one that replaced it. Only the most recent regeneration can be undone.
Requires the reviews:write permission.
Path Parameters
idstring (uuid)requiredThe ID of the product.
curl \
-X POST \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary/revert" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { postProductsByIdReviewSummaryRevert } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await postProductsByIdReviewSummaryRevert({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/review-summary/revert")
req = Net::HTTP::Post.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/products/550e8400-e29b-41d4-a716-446655440000/review-summary/revert",
method="POST",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"productId": "550e8400-e29b-41d4-a716-446655440000",
"summary": "string",
"themes": [
{
"label": "string",
"sentiment": "positive",
"reviewCount": 0
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z",
"suppressedAt": "2025-01-15T09:30:00Z",
"previous": {
"summary": "string",
"themes": [
{
"label": "...",
"sentiment": "...",
"reviewCount": "..."
}
],
"language": "string",
"reviewCount": 0,
"newestReviewAt": "2025-01-15T09:30:00Z",
"modelId": "string",
"promptVersion": 0,
"generatedAt": "2025-01-15T09:30:00Z"
}
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"productId": {
"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 product this summary describes."
},
"summary": {
"type": "string",
"description": "Two or three sentences on what reviewers say, generated from the reviews."
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The language the summary is written in, which is the one most reviews are in."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many published reviews the summary was generated from."
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When the newest review it covers was written."
},
"modelId": {
"type": "string",
"description": "The model that produced it."
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Which version of the instructions produced it."
},
"generatedAt": {
"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 it was generated."
},
"suppressedAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
],
"description": "When it was hidden from the storefront, or null if it is showing."
},
"previous": {
"anyOf": [
{
"type": "object",
"properties": {
"summary": {
"type": "string"
},
"themes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": {
"type": "string",
"description": "Short name for the theme, such as `Fit runs small`, in a few words at most."
},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "mixed"],
"description": "Whether reviewers raise this theme approvingly, as a complaint, or both."
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Roughly how many of the published reviews mention this theme."
}
},
"required": ["label", "sentiment", "reviewCount"],
"additionalProperties": false
},
"description": "Recurring topics reviewers raise, most mentioned first."
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"reviewCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"newestReviewAt": {
"anyOf": [
{
"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))$"
},
{
"type": "null"
}
]
},
"modelId": {
"type": "string"
},
"promptVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"generatedAt": {
"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))$"
}
},
"required": [
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt"
],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The summary this one replaced, kept until it is accepted or reverted to. Null once there is nothing to go back to."
}
},
"required": [
"productId",
"summary",
"themes",
"language",
"reviewCount",
"newestReviewAt",
"modelId",
"promptVersion",
"generatedAt",
"suppressedAt",
"previous"
],
"additionalProperties": false,
"description": "The restored review summary."
}GET /products
List the organization's products, newest first by default. Supports search, filtering and cursor pagination.
Requires the products: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.
sorttitle | -title | createdAt | -createdAt | wishlistsCount | -wishlistsCountThe sort order. Prefix a field with - to sort descending. Defaults to -createdAt.
searchstringA free-text search term, matched case-insensitively against the record's most identifying fields. Combined with filter using AND.
filterstringA JSON-encoded filter document, for example {"createdAt":{"$gte":"2025-01-01T00:00:00Z"}}. Filterable fields: id, slug, title, metadata, createdAt, tags.name, variants.id, variants.title, variants.slug, variants.metadata. See Filtering for the syntax.
includestringA comma-separated list of extra fields to compute and return. Each one costs an additional query, so ask only for what you need. Available: importJob, reviewsCount, wishlistsCount, variantsCount, tags, variants.
curl \
"https://api.cascade.dev/products" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { getProducts } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await getProducts()require "net/http"
uri = URI("https://api.cascade.dev/products")
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/products",
headers={"Authorization": "Bearer sk_YOUR_SECRET_KEY"},
)
with urlopen(req) as res:
print(json.load(res)){
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"notes": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z",
"importJob": {
"id": "...",
"type": "...",
"action": "...",
"row": "...",
"createdAt": "..."
},
"reviewsCount": 0,
"wishlistsCount": 0,
"variantsCount": 0,
"tags": ["..."],
"variants": ["..."]
}
],
"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 product."
},
"slug": {
"type": "string",
"description": "The product's URL handle, unique within the organization. For Shopify-connected stores this is the Shopify handle, so renaming it there creates a new product here."
},
"title": {
"type": "string",
"description": "The product's display name."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The product's long-form description."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's main image."
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's page on the storefront, or null if it is not known."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
},
"notes": {
"type": "string",
"description": "Internal notes about the product. Never shown to customers."
},
"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 product was first created in Cascade."
},
"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 product was last changed."
},
"importJob": {
"anyOf": [
{
"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": "The import job that wrote the record."
},
"type": {
"type": "string",
"enum": [
"customers",
"products",
"orders",
"wishlists",
"reviews",
"loyalty-members"
],
"description": "What that import's file contained."
},
"action": {
"type": "string",
"enum": ["created", "updated"],
"description": "Whether the import created this record or updated one that was already there."
},
"row": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The 1-based row in the import's CSV that became this record, where it is known."
},
"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 that import was started."
}
},
"required": ["id", "type", "action", "row", "createdAt"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The import that last wrote this record, or null when no import did."
},
"reviewsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many reviews the product has."
},
"wishlistsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many wishlists the product is on. Counts wishlists, not items."
},
"variantsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many variants the product has."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Names of the tags applied to the product."
},
"variants": {
"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 variant."
},
"slug": {
"type": "string",
"description": "The variant's handle, unique within its product."
},
"title": {
"type": "string",
"description": "The variant's display name, such as its size or color."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the variant's image."
},
"priceCents": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The variant's stored price in cents, or null if it is not known."
},
"currencyCode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The currency of `priceCents` as an ISO 4217 code, or null if it is not known."
},
"available": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Whether the variant can be bought right now, or null if no source has said. Back-in-stock alerts fire on a false to true change, so an unknown availability never triggers one."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
}
},
"required": [
"id",
"slug",
"title",
"imageUrl",
"priceCents",
"currencyCode",
"available",
"metadata"
],
"additionalProperties": false
},
"description": "The product's variants, sorted by title."
}
},
"required": [
"id",
"slug",
"title",
"description",
"imageUrl",
"url",
"metadata",
"notes",
"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
}PATCH /products/bulk-update
Add or remove tags and metadata across every product matching a filter, in one request. Sending no filter applies the changes to every product in the organization.
Requires the products:write permission.
Costs 5 rate limit tokens instead of the usual one. See Rate limits.
Query Parameters
filterstringA JSON-encoded filter document selecting the products to update. Omit it and every product in the organization is updated. See Filtering for the syntax.
searchstringA free-text search term, matched case-insensitively against the record's most identifying fields. Combined with filter using AND.
Body Parameters
addTagsstring[]Tag names to add. Tags that do not exist yet are created.
removeTagsstring[]Tag names to remove.
addMetadataobjectMetadata keys to set, merged onto whatever each product already has.
removeMetadataKeysstring[]Metadata keys to delete.
curl \
-X PATCH \
"https://api.cascade.dev/products/bulk-update" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"addTags": [
"string"
],
"removeTags": [
"string"
],
"addMetadata": "...",
"removeMetadataKeys": [
"string"
]
}'import { client } from "@cascade-commerce/api/admin/client"
import { patchProductsBulkUpdate } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await patchProductsBulkUpdate({
body: {
addTags: ["string"],
removeTags: ["string"],
addMetadata: "...",
removeMetadataKeys: ["string"],
},
})require "net/http"
uri = URI("https://api.cascade.dev/products/bulk-update")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"addTags": [
"string"
],
"removeTags": [
"string"
],
"addMetadata": "...",
"removeMetadataKeys": [
"string"
]
}
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 = """{
"addTags": [
"string"
],
"removeTags": [
"string"
],
"addMetadata": "...",
"removeMetadataKeys": [
"string"
]
}"""
req = Request(
"https://api.cascade.dev/products/bulk-update",
data=body.encode(),
method="PATCH",
headers={
"Authorization": "Bearer sk_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
)
with urlopen(req) as res:
print(json.load(res)){
"count": 0
}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"count": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many products the filter matched and updated."
}
},
"required": ["count"],
"additionalProperties": false
}POST /products/import
Create or update products in bulk, matching on slug. Variants and site links are replaced with exactly what you send, so include the full set for each product. Rows naming a site that doesn't exist are skipped and returned in skipped; set strict to fail on the first such row instead.
Requires the products:write permission.
Costs 10 rate limit tokens instead of the usual one. See Rate limits.
Body Parameters
dataobject[]requiredThe products to create or update.
strictbooleanWhen true, a row that references something missing fails the whole import with an error. Off by default: rows whose references don't resolve are skipped and reported in skipped.
mergeMetadatabooleanWhen true, each row's metadata keys (and its variants' metadata keys) are merged onto whatever is already stored instead of replacing it.
curl \
-X POST \
"https://api.cascade.dev/products/import" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": [
{
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"tags": [
"..."
],
"variants": [
"..."
],
"sites": [
"..."
]
}
],
"strict": true,
"mergeMetadata": true
}'import { client } from "@cascade-commerce/api/admin/client"
import { postProductsImport } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await postProductsImport({
body: {
data: [
{
slug: "string",
title: "string",
description: "string",
imageUrl: "string",
url: "string",
metadata: "...",
tags: ["..."],
variants: ["..."],
sites: ["..."],
},
],
strict: true,
mergeMetadata: true,
},
})require "net/http"
uri = URI("https://api.cascade.dev/products/import")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"data": [
{
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"tags": [
"..."
],
"variants": [
"..."
],
"sites": [
"..."
]
}
],
"strict": true,
"mergeMetadata": true
}
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": [
{
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"tags": [
"..."
],
"variants": [
"..."
],
"sites": [
"..."
]
}
],
"strict": true,
"mergeMetadata": true
}"""
req = Request(
"https://api.cascade.dev/products/import",
data=body.encode(),
headers={
"Authorization": "Bearer sk_YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
)
with urlopen(req) as res:
print(json.load(res)){
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"notes": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z",
"importJob": {
"id": "...",
"type": "...",
"action": "...",
"row": "...",
"createdAt": "..."
},
"reviewsCount": 0,
"wishlistsCount": 0,
"variantsCount": 0,
"tags": ["..."],
"variants": ["..."]
}
],
"skipped": [
{
"index": 0,
"message": "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 product."
},
"slug": {
"type": "string",
"description": "The product's URL handle, unique within the organization. For Shopify-connected stores this is the Shopify handle, so renaming it there creates a new product here."
},
"title": {
"type": "string",
"description": "The product's display name."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The product's long-form description."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's main image."
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's page on the storefront, or null if it is not known."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
},
"notes": {
"type": "string",
"description": "Internal notes about the product. Never shown to customers."
},
"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 product was first created in Cascade."
},
"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 product was last changed."
},
"importJob": {
"anyOf": [
{
"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": "The import job that wrote the record."
},
"type": {
"type": "string",
"enum": [
"customers",
"products",
"orders",
"wishlists",
"reviews",
"loyalty-members"
],
"description": "What that import's file contained."
},
"action": {
"type": "string",
"enum": ["created", "updated"],
"description": "Whether the import created this record or updated one that was already there."
},
"row": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The 1-based row in the import's CSV that became this record, where it is known."
},
"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 that import was started."
}
},
"required": ["id", "type", "action", "row", "createdAt"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The import that last wrote this record, or null when no import did."
},
"reviewsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many reviews the product has."
},
"wishlistsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many wishlists the product is on. Counts wishlists, not items."
},
"variantsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many variants the product has."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Names of the tags applied to the product."
},
"variants": {
"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 variant."
},
"slug": {
"type": "string",
"description": "The variant's handle, unique within its product."
},
"title": {
"type": "string",
"description": "The variant's display name, such as its size or color."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the variant's image."
},
"priceCents": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The variant's stored price in cents, or null if it is not known."
},
"currencyCode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The currency of `priceCents` as an ISO 4217 code, or null if it is not known."
},
"available": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Whether the variant can be bought right now, or null if no source has said. Back-in-stock alerts fire on a false to true change, so an unknown availability never triggers one."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
}
},
"required": [
"id",
"slug",
"title",
"imageUrl",
"priceCents",
"currencyCode",
"available",
"metadata"
],
"additionalProperties": false
},
"description": "The product's variants, sorted by title."
}
},
"required": [
"id",
"slug",
"title",
"description",
"imageUrl",
"url",
"metadata",
"notes",
"createdAt",
"updatedAt"
],
"additionalProperties": false
},
"description": "The imported products, as they now stand."
},
"skipped": {
"type": "array",
"items": {
"type": "object",
"properties": {
"index": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "Position of the row in the batch you sent."
},
"message": {
"type": "string",
"description": "Why the row could not be imported."
}
},
"required": ["index", "message"],
"additionalProperties": false
},
"description": "The rows that could not be imported, and why."
}
},
"required": ["data", "skipped"],
"additionalProperties": false
}GET /products/{id}
Get a single product by ID.
Requires the products:read permission.
Path Parameters
idstring (uuid)requiredThe ID of the product.
Query Parameters
includestringA comma-separated list of extra fields to compute and return. Each one costs an additional query, so ask only for what you need. Available: importJob, reviewsCount, wishlistsCount, variantsCount, tags, variants.
curl \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { getProductsById } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await getProductsById({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/products/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/products/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",
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"notes": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z",
"importJob": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "customers",
"action": "created",
"row": 0,
"createdAt": "2025-01-15T09:30:00Z"
},
"reviewsCount": 0,
"wishlistsCount": 0,
"variantsCount": 0,
"tags": ["string"],
"variants": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "string",
"title": "string",
"imageUrl": "string",
"priceCents": 0,
"currencyCode": "string",
"available": true,
"metadata": "..."
}
]
}{
"$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 product."
},
"slug": {
"type": "string",
"description": "The product's URL handle, unique within the organization. For Shopify-connected stores this is the Shopify handle, so renaming it there creates a new product here."
},
"title": {
"type": "string",
"description": "The product's display name."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The product's long-form description."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's main image."
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's page on the storefront, or null if it is not known."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
},
"notes": {
"type": "string",
"description": "Internal notes about the product. Never shown to customers."
},
"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 product was first created in Cascade."
},
"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 product was last changed."
},
"importJob": {
"anyOf": [
{
"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": "The import job that wrote the record."
},
"type": {
"type": "string",
"enum": [
"customers",
"products",
"orders",
"wishlists",
"reviews",
"loyalty-members"
],
"description": "What that import's file contained."
},
"action": {
"type": "string",
"enum": ["created", "updated"],
"description": "Whether the import created this record or updated one that was already there."
},
"row": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The 1-based row in the import's CSV that became this record, where it is known."
},
"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 that import was started."
}
},
"required": ["id", "type", "action", "row", "createdAt"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The import that last wrote this record, or null when no import did."
},
"reviewsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many reviews the product has."
},
"wishlistsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many wishlists the product is on. Counts wishlists, not items."
},
"variantsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many variants the product has."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Names of the tags applied to the product."
},
"variants": {
"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 variant."
},
"slug": {
"type": "string",
"description": "The variant's handle, unique within its product."
},
"title": {
"type": "string",
"description": "The variant's display name, such as its size or color."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the variant's image."
},
"priceCents": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The variant's stored price in cents, or null if it is not known."
},
"currencyCode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The currency of `priceCents` as an ISO 4217 code, or null if it is not known."
},
"available": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Whether the variant can be bought right now, or null if no source has said. Back-in-stock alerts fire on a false to true change, so an unknown availability never triggers one."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
}
},
"required": [
"id",
"slug",
"title",
"imageUrl",
"priceCents",
"currencyCode",
"available",
"metadata"
],
"additionalProperties": false
},
"description": "The product's variants, sorted by title."
}
},
"required": [
"id",
"slug",
"title",
"description",
"imageUrl",
"url",
"metadata",
"notes",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
"description": "The requested product."
}PATCH /products/{id}
Update a product's metadata, notes or tags. Catalog fields such as the title and slug come from the store and cannot be edited here. Triggers the product.updated webhook.
Requires the products:write permission.
Path Parameters
idstring (uuid)requiredThe ID of the product to update.
Body Parameters
metadataobjectReplaces the product's metadata wholesale. Omit to leave it alone.
notesstringInternal notes about the product.
tagsstring[]The product's full set of tag names. Omit to leave the tags alone.
curl \
-X PATCH \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"metadata": "...",
"notes": "string",
"tags": [
"string"
]
}'import { client } from "@cascade-commerce/api/admin/client"
import { patchProductsById } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await patchProductsById({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
body: {
metadata: "...",
notes: "string",
tags: ["string"],
},
})require "net/http"
uri = URI("https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"metadata": "...",
"notes": "string",
"tags": [
"string"
]
}
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 = """{
"metadata": "...",
"notes": "string",
"tags": [
"string"
]
}"""
req = Request(
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000",
data=body.encode(),
method="PATCH",
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",
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"notes": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z",
"importJob": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "customers",
"action": "created",
"row": 0,
"createdAt": "2025-01-15T09:30:00Z"
},
"reviewsCount": 0,
"wishlistsCount": 0,
"variantsCount": 0,
"tags": ["string"],
"variants": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "string",
"title": "string",
"imageUrl": "string",
"priceCents": 0,
"currencyCode": "string",
"available": true,
"metadata": "..."
}
]
}{
"$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 product."
},
"slug": {
"type": "string",
"description": "The product's URL handle, unique within the organization. For Shopify-connected stores this is the Shopify handle, so renaming it there creates a new product here."
},
"title": {
"type": "string",
"description": "The product's display name."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The product's long-form description."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's main image."
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's page on the storefront, or null if it is not known."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
},
"notes": {
"type": "string",
"description": "Internal notes about the product. Never shown to customers."
},
"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 product was first created in Cascade."
},
"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 product was last changed."
},
"importJob": {
"anyOf": [
{
"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": "The import job that wrote the record."
},
"type": {
"type": "string",
"enum": [
"customers",
"products",
"orders",
"wishlists",
"reviews",
"loyalty-members"
],
"description": "What that import's file contained."
},
"action": {
"type": "string",
"enum": ["created", "updated"],
"description": "Whether the import created this record or updated one that was already there."
},
"row": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The 1-based row in the import's CSV that became this record, where it is known."
},
"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 that import was started."
}
},
"required": ["id", "type", "action", "row", "createdAt"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The import that last wrote this record, or null when no import did."
},
"reviewsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many reviews the product has."
},
"wishlistsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many wishlists the product is on. Counts wishlists, not items."
},
"variantsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many variants the product has."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Names of the tags applied to the product."
},
"variants": {
"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 variant."
},
"slug": {
"type": "string",
"description": "The variant's handle, unique within its product."
},
"title": {
"type": "string",
"description": "The variant's display name, such as its size or color."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the variant's image."
},
"priceCents": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The variant's stored price in cents, or null if it is not known."
},
"currencyCode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The currency of `priceCents` as an ISO 4217 code, or null if it is not known."
},
"available": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Whether the variant can be bought right now, or null if no source has said. Back-in-stock alerts fire on a false to true change, so an unknown availability never triggers one."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
}
},
"required": [
"id",
"slug",
"title",
"imageUrl",
"priceCents",
"currencyCode",
"available",
"metadata"
],
"additionalProperties": false
},
"description": "The product's variants, sorted by title."
}
},
"required": [
"id",
"slug",
"title",
"description",
"imageUrl",
"url",
"metadata",
"notes",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
"description": "The updated product."
}DELETE /products/{id}
Delete a product along with its variants, tags and site links. Its reviews are kept but unlinked and unpublished, unless the organization has cascadeDeleteReviews turned on. Triggers the product.deleted webhook.
Requires the products:write permission.
Path Parameters
idstring (uuid)requiredThe ID of the product to delete.
curl \
-X DELETE \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY"import { client } from "@cascade-commerce/api/admin/client"
import { deleteProductsById } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await deleteProductsById({
path: { id: "550e8400-e29b-41d4-a716-446655440000" },
})require "net/http"
uri = URI("https://api.cascade.dev/products/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/products/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",
"slug": "string",
"title": "string",
"description": "string",
"imageUrl": "string",
"url": "string",
"metadata": "...",
"notes": "string",
"createdAt": "2025-01-15T09:30:00Z",
"updatedAt": "2025-01-15T09:30:00Z",
"importJob": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "customers",
"action": "created",
"row": 0,
"createdAt": "2025-01-15T09:30:00Z"
},
"reviewsCount": 0,
"wishlistsCount": 0,
"variantsCount": 0,
"tags": ["string"],
"variants": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"slug": "string",
"title": "string",
"imageUrl": "string",
"priceCents": 0,
"currencyCode": "string",
"available": true,
"metadata": "..."
}
]
}{
"$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 product."
},
"slug": {
"type": "string",
"description": "The product's URL handle, unique within the organization. For Shopify-connected stores this is the Shopify handle, so renaming it there creates a new product here."
},
"title": {
"type": "string",
"description": "The product's display name."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The product's long-form description."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's main image."
},
"url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the product's page on the storefront, or null if it is not known."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
},
"notes": {
"type": "string",
"description": "Internal notes about the product. Never shown to customers."
},
"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 product was first created in Cascade."
},
"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 product was last changed."
},
"importJob": {
"anyOf": [
{
"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": "The import job that wrote the record."
},
"type": {
"type": "string",
"enum": [
"customers",
"products",
"orders",
"wishlists",
"reviews",
"loyalty-members"
],
"description": "What that import's file contained."
},
"action": {
"type": "string",
"enum": ["created", "updated"],
"description": "Whether the import created this record or updated one that was already there."
},
"row": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The 1-based row in the import's CSV that became this record, where it is known."
},
"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 that import was started."
}
},
"required": ["id", "type", "action", "row", "createdAt"],
"additionalProperties": false
},
{
"type": "null"
}
],
"description": "The import that last wrote this record, or null when no import did."
},
"reviewsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many reviews the product has."
},
"wishlistsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many wishlists the product is on. Counts wishlists, not items."
},
"variantsCount": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991,
"description": "How many variants the product has."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Names of the tags applied to the product."
},
"variants": {
"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 variant."
},
"slug": {
"type": "string",
"description": "The variant's handle, unique within its product."
},
"title": {
"type": "string",
"description": "The variant's display name, such as its size or color."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the variant's image."
},
"priceCents": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The variant's stored price in cents, or null if it is not known."
},
"currencyCode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The currency of `priceCents` as an ISO 4217 code, or null if it is not known."
},
"available": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Whether the variant can be bought right now, or null if no source has said. Back-in-stock alerts fire on a false to true change, so an unknown availability never triggers one."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
}
},
"required": [
"id",
"slug",
"title",
"imageUrl",
"priceCents",
"currencyCode",
"available",
"metadata"
],
"additionalProperties": false
},
"description": "The product's variants, sorted by title."
}
},
"required": [
"id",
"slug",
"title",
"description",
"imageUrl",
"url",
"metadata",
"notes",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
"description": "The deleted product."
}PATCH /products/{productId}/variants/{variantId}
Update a variant's metadata. Triggers the productVariant.updated webhook.
Requires the products:write permission.
Path Parameters
productIdstring (uuid)requiredThe ID of the product the variant belongs to.
variantIdstring (uuid)requiredThe ID of the variant to update.
Body Parameters
metadataobjectReplaces the variant's metadata wholesale. Omit to leave it alone.
curl \
-X PATCH \
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/variants/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer sk_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"metadata": "..."
}'import { client } from "@cascade-commerce/api/admin/client"
import { patchProductsByProductIdVariantsByVariantId } from "@cascade-commerce/api/admin"
client.setConfig({
baseUrl: "https://api.cascade.dev",
headers: { Authorization: "Bearer sk_YOUR_SECRET_KEY" },
})
const { data, error } = await patchProductsByProductIdVariantsByVariantId({
path: {
productId: "550e8400-e29b-41d4-a716-446655440000",
variantId: "550e8400-e29b-41d4-a716-446655440000",
},
body: {
metadata: "...",
},
})require "net/http"
uri = URI("https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/variants/550e8400-e29b-41d4-a716-446655440000")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer sk_YOUR_SECRET_KEY"
req["Content-Type"] = "application/json"
req.body = <<~JSON
{
"metadata": "..."
}
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 = """{
"metadata": "..."
}"""
req = Request(
"https://api.cascade.dev/products/550e8400-e29b-41d4-a716-446655440000/variants/550e8400-e29b-41d4-a716-446655440000",
data=body.encode(),
method="PATCH",
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",
"slug": "string",
"title": "string",
"imageUrl": "string",
"priceCents": 0,
"currencyCode": "string",
"available": true,
"metadata": "..."
}{
"$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 variant."
},
"slug": {
"type": "string",
"description": "The variant's handle, unique within its product."
},
"title": {
"type": "string",
"description": "The variant's display name, such as its size or color."
},
"imageUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "URL of the variant's image."
},
"priceCents": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
],
"description": "The variant's stored price in cents, or null if it is not known."
},
"currencyCode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The currency of `priceCents` as an ISO 4217 code, or null if it is not known."
},
"available": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Whether the variant can be bought right now, or null if no source has said. Back-in-stock alerts fire on a false to true change, so an unknown availability never triggers one."
},
"metadata": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
},
"description": "Free-form string key/value pairs for your own data. Individual keys are filterable with a dot path, for example `metadata.category`."
}
},
"required": [
"id",
"slug",
"title",
"imageUrl",
"priceCents",
"currencyCode",
"available",
"metadata"
],
"additionalProperties": false,
"description": "The updated variant."
}