Create a Product with Variants
A common workflow: create a product, then add options and variants for sizes and colors.
Step 1: Create the product:
curl -X POST https://yourshop.zeroshop.io/admin/api/products \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"name": "Classic Hoodie", "price": 5999, "product_type": "physical", "visible": true}'
# Note the product ID from the response (e.g., 42)
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/products",
headers=headers,
json={"name": "Classic Hoodie", "price": 5999, "product_type": "physical", "visible": True}
)
product_id = resp.json()["id"]
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/products", {
method: "POST", headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify({ name: "Classic Hoodie", price: 5999, product_type: "physical", visible: true })
});
const { id: productId } = await resp.json();
req = Net::HTTP::Post.new(URI("https://yourshop.zeroshop.io/admin/api/products"))
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { name: "Classic Hoodie", price: 5999, product_type: "physical", visible: true }.to_json
resp = Net::HTTP.start("yourshop.zeroshop.io", 443, use_ssl: true) { |http| http.request(req) }
product_id = JSON.parse(resp.body)["id"]
Step 2: Save options and variants (this creates Size and Color options plus all variant combinations):
curl -X PUT https://yourshop.zeroshop.io/admin/api/products/42/variants \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{
"options": [
{"name": "Size", "values": ["S", "M", "L", "XL"]},
{"name": "Color", "values": ["Black", "Navy"]}
],
"variants": [
{"options": {"Size": "S", "Color": "Black"}, "price": 5999, "sku": "HOODIE-S-BLK"},
{"options": {"Size": "M", "Color": "Black"}, "price": 5999, "sku": "HOODIE-M-BLK"},
{"options": {"Size": "L", "Color": "Black"}, "price": 5999, "sku": "HOODIE-L-BLK"},
{"options": {"Size": "XL", "Color": "Black"}, "price": 6499, "sku": "HOODIE-XL-BLK"},
{"options": {"Size": "S", "Color": "Navy"}, "price": 5999, "sku": "HOODIE-S-NAV"},
{"options": {"Size": "M", "Color": "Navy"}, "price": 5999, "sku": "HOODIE-M-NAV"},
{"options": {"Size": "L", "Color": "Navy"}, "price": 5999, "sku": "HOODIE-L-NAV"},
{"options": {"Size": "XL", "Color": "Navy"}, "price": 6499, "sku": "HOODIE-XL-NAV"}
]
}'
requests.put(
f"https://yourshop.zeroshop.io/admin/api/products/{product_id}/variants",
headers=headers,
json={
"options": [
{"name": "Size", "values": ["S", "M", "L", "XL"]},
{"name": "Color", "values": ["Black", "Navy"]}
],
"variants": [
{"options": {"Size": "S", "Color": "Black"}, "price": 5999, "sku": "HOODIE-S-BLK"},
{"options": {"Size": "M", "Color": "Black"}, "price": 5999, "sku": "HOODIE-M-BLK"},
# ... remaining variants
]
}
)
await fetch(`https://yourshop.zeroshop.io/admin/api/products/${productId}/variants`, {
method: "PUT",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify({
options: [
{ name: "Size", values: ["S", "M", "L", "XL"] },
{ name: "Color", values: ["Black", "Navy"] }
],
variants: [
{ options: { Size: "S", Color: "Black" }, price: 5999, sku: "HOODIE-S-BLK" },
{ options: { Size: "M", Color: "Black" }, price: 5999, sku: "HOODIE-M-BLK" },
// ... remaining variants
]
})
});
req = Net::HTTP::Put.new(URI("https://yourshop.zeroshop.io/admin/api/products/#{product_id}/variants"))
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = {
options: [
{ name: "Size", values: %w[S M L XL] },
{ name: "Color", values: %w[Black Navy] }
],
variants: [
{ options: { Size: "S", Color: "Black" }, price: 5999, sku: "HOODIE-S-BLK" },
{ options: { Size: "M", Color: "Black" }, price: 5999, sku: "HOODIE-M-BLK" },
# ... remaining variants
]
}.to_json
Net::HTTP.start("yourshop.zeroshop.io", 443, use_ssl: true) { |http| http.request(req) }
Process an Order
When a new order comes in, you typically update its status through these stages:
- Confirm payment:
PATCH /admin/api/orders/{id}/payment-statuswith{"payment_status": "paid"} - Start processing:
PATCH /admin/api/orders/{id}/statuswith{"status": "processing"} - Ship the order:
PATCH /admin/api/orders/{id}/shippingwith tracking info - Mark delivered:
PATCH /admin/api/orders/{id}/statuswith{"status": "delivered"}
# Mark as shipped with tracking
curl -X PATCH https://yourshop.zeroshop.io/admin/api/orders/123/shipping \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"tracking_number": "1Z999AA10123456784", "carrier": "UPS"}'
requests.patch(
"https://yourshop.zeroshop.io/admin/api/orders/123/shipping",
headers=headers,
json={"tracking_number": "1Z999AA10123456784", "carrier": "UPS"}
)
await fetch("https://yourshop.zeroshop.io/admin/api/orders/123/shipping", {
method: "PATCH",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify({ tracking_number: "1Z999AA10123456784", carrier: "UPS" })
});
req = Net::HTTP::Patch.new(URI("https://yourshop.zeroshop.io/admin/api/orders/123/shipping"))
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { tracking_number: "1Z999AA10123456784", carrier: "UPS" }.to_json
Net::HTTP.start("yourshop.zeroshop.io", 443, use_ssl: true) { |http| http.request(req) }
Bulk Import Products
To import many products, use idempotency keys to safely retry on failure. Assign a unique
key to each row (e.g., csv-row-{n}) so retries don't create duplicates:
curl -X POST https://yourshop.zeroshop.io/admin/api/products \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: csv-row-1-2026-04-13" \
-d '{"name": "Widget A", "price": 999}'
import csv
with open("products.csv") as f:
for i, row in enumerate(csv.DictReader(f)):
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/products",
headers={**headers, "Idempotency-Key": f"csv-row-{i}-2026-04-13"},
json={"name": row["name"], "price": int(row["price_cents"])}
)
if not resp.ok:
print(f"Row {i} failed: {resp.json()['error']}")
for (const [i, row] of products.entries()) {
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/products", {
method: "POST",
headers: { ...authHeaders, "Content-Type": "application/json", "Idempotency-Key": `csv-row-${i}-2026-04-13` },
body: JSON.stringify({ name: row.name, price: row.priceCents })
});
if (!resp.ok) {
const { error } = await resp.json();
console.error(`Row ${i} failed: ${error}`);
}
}
require "csv"
CSV.foreach("products.csv", headers: true).with_index do |row, i|
req = Net::HTTP::Post.new(URI("https://yourshop.zeroshop.io/admin/api/products"))
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "csv-row-#{i}-2026-04-13"
req.body = { name: row["name"], price: row["price_cents"].to_i }.to_json
resp = Net::HTTP.start("yourshop.zeroshop.io", 443, use_ssl: true) { |http| http.request(req) }
puts "Row #{i} failed: #{JSON.parse(resp.body)['error']}" unless resp.is_a?(Net::HTTPSuccess)
end