Manage product options like Size and Color
/admin/api/options
List options with pagination, search, type filtering, and sorting. Returns summary rows with value counts by default. Pass include_values=true for full detail with values and translations.
/admin/api/options
Create a new option. A URL slug is auto-derived from the name. Type defaults to "variant" if omitted.
/admin/api/options/{id}
Fetch a single option with its values and content translations.
/admin/api/options/{id}
Update an option's name, root listing visibility, and translations. Re-derives the slug from the new name.
/admin/api/options/{id}
Delete an option and all its values.
/admin/api/options/{id}/values
Add a value to an option. A URL slug is auto-derived from the value and ensured unique within the option.
/admin/api/options/{id}/values/reorder
Reorder the values of an option. Provide the complete list of value IDs in the desired order. All IDs must belong to the given option.
/admin/api/options/{id}/values/{vid}
Delete an option value.
/admin/api/options/{id}/values/{vid}/image
Upload or replace the swatch image for an option value (typically used for color or pattern swatches). Accepts multipart/form-data with a single field named "file".
/admin/api/options/{id}/values/{vid}/image
Remove the swatch image for an option value. Idempotent — succeeds even if no image is set.
/admin/api/options/{id}/values/{vid}/value
Update the displayed label of an option value (e.g. rename `M` to `Medium`). Whitespace is trimmed; the slug is not re-derived — use the slug endpoint for that.
/admin/api/options/{id}/values/{vid}/slug
Update the slug of an option value. The submitted text is run through `slugify`, so the caller may pass either a pre-slugified string or free text. Within the parent option, the slug is conflict-checked (excluding the current value), and a numeric suffix is appended if necessary to keep it unique. Existing variant slug redirects continue to honour the old slug.
/admin/api/options/{id}/values/{vid}/description
Set or clear the buyer-facing description for an option value (e.g. tooltip text on a swatch). Empty string clears; max 500 characters.
/admin/api/options
List options with pagination, search, type filtering, and sorting. Returns summary rows with value counts by default. Pass include_values=true for full detail with values and translations.
| Name | Type | Required | Description |
|---|---|---|---|
| page | integer | optional | Page number, 1-indexed (default: 1) |
| per_page | integer | optional | Items per page, 1-100 (default: 20) |
| search | string | optional | Case-insensitive text search on option name |
| option_type | string | optional | Filter by type: "variant" or "attribute" |
| sort | string | optional | Sort field with optional - prefix for descending. Valid: name, sort_order, option_type. Default: sort_order |
| include_values | boolean | optional | When true, returns full option detail including values and translations instead of summary rows |
{
"data": [
{
"id": 1,
"name": "Size",
"slug": "size",
"sort_order": 0,
"option_type": "variant",
"show_in_root_listing": true,
"values_count": 4
},
{
"id": 2,
"name": "Color",
"slug": "color",
"sort_order": 1,
"option_type": "variant",
"show_in_root_listing": true,
"values_count": 6
},
{
"id": 3,
"name": "Material",
"slug": "material",
"sort_order": 2,
"option_type": "attribute",
"show_in_root_listing": false,
"values_count": 3
}
],
"page": 1,
"per_page": 20,
"total": 3,
"total_pages": 1
}
curl "https://yourshop.zeroshop.io/admin/api/options?page=1&per_page=20&option_type=variant" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/options",
params={"page": 1, "per_page": 20, "option_type": "variant"},
headers={"Authorization": "Bearer zspat_..."}
)
options = resp.json()
const params = new URLSearchParams({ page: 1, per_page: 20, option_type: "variant" });
const resp = await fetch(
`https://yourshop.zeroshop.io/admin/api/options?${params}`,
{ headers: { "Authorization": "Bearer zspat_..." } }
);
const data = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options?page=1&per_page=20&option_type=variant")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(resp.body)
/admin/api/options
Create a new option. A URL slug is auto-derived from the name. Type defaults to "variant" if omitted.
{
"name": "Size",
"option_type": "variant",
"show_in_root_listing": true,
"translations": [
{
"locale": "de",
"field": "name",
"value": "Groesse"
}
]
}
{
"id": 1,
"name": "Size",
"slug": "size"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/options" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Size",
"option_type": "variant",
"show_in_root_listing": true,
"translations": [
{ "locale": "de", "field": "name", "value": "Groesse" }
]
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/options",
headers={"Authorization": "Bearer zspat_..."},
json={
"name": "Size",
"option_type": "variant",
"show_in_root_listing": True,
"translations": [
{"locale": "de", "field": "name", "value": "Groesse"}
]
}
)
option = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Size",
option_type: "variant",
show_in_root_listing: true,
translations: [
{ locale: "de", field: "name", value: "Groesse" }
]
})
});
const option = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
name: "Size",
option_type: "variant",
show_in_root_listing: true,
translations: [
{ locale: "de", field: "name", value: "Groesse" }
]
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
option = JSON.parse(resp.body)
/admin/api/options/{id}
Fetch a single option with its values and content translations.
{
"id": 1,
"name": "Size",
"slug": "size",
"sort_order": 0,
"option_type": "variant",
"show_in_root_listing": true,
"values": [
{
"id": 10,
"value": "S",
"slug": "s",
"sort_order": 0
},
{
"id": 11,
"value": "M",
"slug": "m",
"sort_order": 1
},
{
"id": 12,
"value": "L",
"slug": "l",
"sort_order": 2
},
{
"id": 13,
"value": "XL",
"slug": "xl",
"sort_order": 3
}
],
"translations": {
"de": {
"name": "Groesse"
}
}
}
curl "https://yourshop.zeroshop.io/admin/api/options/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/options/1",
headers={"Authorization": "Bearer zspat_..."}
)
option = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options/1", {
headers: { "Authorization": "Bearer zspat_..." }
});
const option = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
option = JSON.parse(resp.body)
/admin/api/options/{id}
Update an option's name, root listing visibility, and translations. Re-derives the slug from the new name.
{
"name": "Shoe Size",
"show_in_root_listing": false,
"translations": [
{
"locale": "de",
"field": "name",
"value": "Schuhgroesse"
}
]
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/options/1" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Shoe Size",
"show_in_root_listing": false,
"translations": [
{ "locale": "de", "field": "name", "value": "Schuhgroesse" }
]
}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/options/1",
headers={"Authorization": "Bearer zspat_..."},
json={
"name": "Shoe Size",
"show_in_root_listing": False,
"translations": [
{"locale": "de", "field": "name", "value": "Schuhgroesse"}
]
}
)
# 204 No Content on success
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options/1", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Shoe Size",
show_in_root_listing: false,
translations: [
{ locale: "de", field: "name", value: "Schuhgroesse" }
]
})
});
// 204 No Content on success
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
name: "Shoe Size",
show_in_root_listing: false,
translations: [
{ locale: "de", field: "name", value: "Schuhgroesse" }
]
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/options/{id}
Delete an option and all its values.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/options/1" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/options/1",
headers={"Authorization": "Bearer zspat_..."}
)
# 204 No Content on success
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options/1", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
// 204 No Content on success
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/options/{id}/values
Add a value to an option. A URL slug is auto-derived from the value and ensured unique within the option.
{
"value": "XXL",
"translations": [
{
"locale": "de",
"field": "value",
"value": "XXL"
}
]
}
{
"id": 14,
"value": "XXL",
"slug": "xxl"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/options/1/values" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"value": "XXL",
"translations": [
{ "locale": "de", "field": "value", "value": "XXL" }
]
}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/options/1/values",
headers={"Authorization": "Bearer zspat_..."},
json={
"value": "XXL",
"translations": [
{"locale": "de", "field": "value", "value": "XXL"}
]
}
)
option_value = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
value: "XXL",
translations: [
{ locale: "de", field: "value", value: "XXL" }
]
})
});
const optionValue = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
value: "XXL",
translations: [
{ locale: "de", field: "value", value: "XXL" }
]
}.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
option_value = JSON.parse(resp.body)
/admin/api/options/{id}/values/reorder
Reorder the values of an option. Provide the complete list of value IDs in the desired order. All IDs must belong to the given option.
{
"ordered_ids": [
12,
11,
10,
13
]
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/options/1/values/reorder" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{ "ordered_ids": [12, 11, 10, 13] }'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/options/1/values/reorder",
headers={"Authorization": "Bearer zspat_..."},
json={"ordered_ids": [12, 11, 10, 13]}
)
# 204 No Content on success
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values/reorder", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ ordered_ids: [12, 11, 10, 13] })
});
// 204 No Content on success
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values/reorder")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { ordered_ids: [12, 11, 10, 13] }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/options/{id}/values/{vid}
Delete an option value.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/options/1/values/13" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/options/1/values/13",
headers={"Authorization": "Bearer zspat_..."}
)
# 204 No Content on success
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values/13", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
// 204 No Content on success
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values/13")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/options/{id}/values/{vid}/image
Upload or replace the swatch image for an option value (typically used for color or pattern swatches). Accepts multipart/form-data with a single field named "file".
| Name | Type | Required | Description |
|---|---|---|---|
| file | file | required | The image file (JPEG, PNG, GIF, or WebP). |
{
"image_url": "https://cdn.zeroshop.io/tenants/abc/option-values/12/4c9a21.webp"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/options/1/values/12/image" \
-H "Authorization: Bearer zspat_..." \
-F "file=@red-swatch.png"
import requests
with open("red-swatch.png", "rb") as f:
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/options/1/values/12/image",
headers={"Authorization": "Bearer zspat_..."},
files={"file": f}
)
image = resp.json()
const form = new FormData();
form.append("file", fileInput.files[0]);
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values/12/image", {
method: "POST",
headers: { "Authorization": "Bearer zspat_..." },
body: form
});
const image = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values/12/image")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
form = [["file", File.open("red-swatch.png")]]
req.set_form(form, "multipart/form-data")
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
image = JSON.parse(resp.body)
/admin/api/options/{id}/values/{vid}/image
Remove the swatch image for an option value. Idempotent — succeeds even if no image is set.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/options/1/values/12/image" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/options/1/values/12/image",
headers={"Authorization": "Bearer zspat_..."}
)
# 204 No Content on success
await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values/12/image", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
// 204 No Content on success
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values/12/image")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/options/{id}/values/{vid}/value
Update the displayed label of an option value (e.g. rename `M` to `Medium`). Whitespace is trimmed; the slug is not re-derived — use the slug endpoint for that.
{
"value": "Medium"
}
{
"value": "Medium"
}
curl -X PATCH "https://yourshop.zeroshop.io/admin/api/options/1/values/11/value" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"value": "Medium"}'
import requests
requests.patch(
"https://yourshop.zeroshop.io/admin/api/options/1/values/11/value",
json={"value": "Medium"},
headers={"Authorization": "Bearer zspat_..."}
)
await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values/11/value", {
method: "PATCH",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ value: "Medium" })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values/11/value")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { value: "Medium" }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/options/{id}/values/{vid}/slug
Update the slug of an option value. The submitted text is run through `slugify`, so the caller may pass either a pre-slugified string or free text. Within the parent option, the slug is conflict-checked (excluding the current value), and a numeric suffix is appended if necessary to keep it unique. Existing variant slug redirects continue to honour the old slug.
{
"slug": "medium"
}
{
"slug": "medium"
}
curl -X PATCH "https://yourshop.zeroshop.io/admin/api/options/1/values/11/slug" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"slug": "medium"}'
import requests
requests.patch(
"https://yourshop.zeroshop.io/admin/api/options/1/values/11/slug",
json={"slug": "medium"},
headers={"Authorization": "Bearer zspat_..."}
)
await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values/11/slug", {
method: "PATCH",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ slug: "medium" })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values/11/slug")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { slug: "medium" }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/options/{id}/values/{vid}/description
Set or clear the buyer-facing description for an option value (e.g. tooltip text on a swatch). Empty string clears; max 500 characters.
{
"description": "100% combed cotton with a slight stretch."
}
curl -X PATCH "https://yourshop.zeroshop.io/admin/api/options/1/values/3/description" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"description":"100% combed cotton with a slight stretch."}'
import requests
requests.patch(
"https://yourshop.zeroshop.io/admin/api/options/1/values/3/description",
json={"description": "100% combed cotton with a slight stretch."},
headers={"Authorization": "Bearer zspat_..."}
)
await fetch("https://yourshop.zeroshop.io/admin/api/options/1/values/3/description", {
method: "PATCH",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ description: "100% combed cotton with a slight stretch." })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/options/1/values/3/description")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { description: "100% combed cotton with a slight stretch." }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
We value your privacy
We use cookies for essential site functionality and, with your consent, analytics to understand how our platform is used. No personal data is shared with third parties. See our Privacy Policy for details.