Products

Create, update, and manage products, variants, images, and digital files

GET /admin/api/products

List products with pagination, filtering, sorting, and search.

POST /admin/api/products

Create a new product. A URL slug is auto-derived from the name. Preview-tier shops are capped at 100 products — exceeding the cap returns 402 with `error: tier_limit_exceeded`, `limit: max_products`. See the Tenant Tier reference.

GET /admin/api/products/{id}

Fetch a single product by its numeric ID. Includes content translations.

PUT /admin/api/products/{id}

Replace all editable fields of an existing product. Re-derives the slug if the name changes.

DELETE /admin/api/products/{id}

Soft-delete a product. The row is kept to preserve order history.

GET /admin/api/products/export.csv

Stream a CSV of products and variants matching the current list filters. The editable columns are `price`, `compare_at_price`, and `stock`; everything else is read-only context. `category` is always included; per-option columns are appended when `include_options=true` so the spreadsheet stays searchable by attribute (e.g. Brand=Apple).

POST /admin/api/products/import/preview

Upload a CSV (multipart, single `file` field) and get a parsed preview of the changes that would be applied. Nothing is written. Re-uploading the same file to `/import/confirm` performs the writes. The CSV must include the required columns: kind, id, product_id, product_name, variant_label, sku, price, compare_at_price, stock, updated_at. Only `price`, `compare_at_price`, and `stock` are editable; other columns are matched for identity or treated as read-only context. Auto-detects `,`, `;`, or tab delimiters. Max 20 MB and 100,000 rows.

POST /admin/api/products/import/confirm

Re-upload the same CSV that was sent to `/import/preview` to apply the changes. The server re-parses, re-validates, and writes all updates in a single transaction so DB drift between preview and confirm is caught. By default any row-level error returns 422 with the full error list; pass `skip_invalid_rows=true` to drop invalid rows and apply the rest. File-level errors (parse, missing column, oversized) always return 422.

GET /admin/api/products/{id}/variants

Get the options and variants for a product. Options are tenant-level; variants are product-level combinations.

PUT /admin/api/products/{id}/variants

Replace all options and variants for a product. Each variant must specify a value for every option.

GET /admin/api/products/{id}/attributes

Get the attribute values currently assigned to a product. Returns option/value pairs grouped by attribute.

PUT /admin/api/products/{id}/attributes

Replace all attribute values for a product. Each referenced option must have option_type 'attribute'.

GET /admin/api/products/{id}/images

List all images for a product, ordered by sort_order.

POST /admin/api/products/{id}/images

Upload a product image via multipart form. The image is resized to max 1200x1200px and re-encoded as WebP.

PUT /admin/api/products/{id}/images/reorder

Reorder images for a product. All image IDs must belong to the product.

DELETE /admin/api/products/{id}/images/{img_id}

Delete a product image. Removes the record and the file from object storage.

GET /admin/api/files

List every digital file in the tenant's library.

POST /admin/api/files

Upload a new digital file via multipart form (single `file` field). Files are stored as-is (no processing) and added to the tenant's flat file library. To make a file available to customers, attach it to one or more access groups via PUT /admin/api/access-groups/{id}/files. Tier caps are enforced — exceeding the per-file or aggregate digital-bytes cap returns 402 with `error: tier_limit_exceeded`. The request body limit is 128 MiB.

DELETE /admin/api/files/{file_id}

Delete a digital file from the library. Removes the record and the underlying object from storage. Cascades to remove the file from any access groups it was attached to.

GET /admin/api/products/{id}/recommendations

List all cross-sell and upsell recommendation links for a product.

PUT /admin/api/products/{id}/recommendations

Replace all recommendations of a given type for a product. Pass an empty target list to clear that type.

DELETE /admin/api/products/{id}/recommendations/{rec_id}

Delete a single recommendation link by its ID.

GET /admin/api/products/{id}/bundle-items

List all bundle components for a bundle product.

PUT /admin/api/products/{id}/bundle-items

Replace all bundle components for a product. Pass an empty items list to clear the bundle.

GET /admin/api/products/{id}/custom-fields

Get the custom fields currently linked to a product.

PUT /admin/api/products/{id}/custom-fields

Replace the set of custom fields linked to a product. Pass custom field IDs in display order.

GET /admin/api/products/{id}/categories

Get the category IDs currently assigned to a product.

PUT /admin/api/products/{id}/categories

Replace all category assignments for a product. Pass an empty list to clear all assignments.

PATCH /admin/api/products/bulk

Apply the same set of boolean field overrides to many products in a single round trip. Pass the product `ids` (1–500) and a `changes` object; only the `changes` fields you set are written, the rest are left untouched. At least one `changes` field must be present. Soft-deleted rows in `ids` are silently ignored. Returns the count of distinct live products that matched.

POST /admin/api/products/{id}/clone

Deep-clone an existing product, including its images (each image is server-side-copied to a fresh object-storage key under the new product so deletes on either side stay isolated). The clone is hidden by default. Counts against the tenant product cap — exceeding it returns 402 with `error: tier_limit_exceeded`, `limit: max_products`. Returns the new product's id and slug.

GET /admin/api/products/create-import/example.csv

Download a filled-in example CSV template that demonstrates all 39 supported columns (Handle, Name, Description, Product Type, Price, variants, images, categories, …) with two example products. Use it as the starting point for a create-import.

POST /admin/api/products/create-import/preview

Upload a create-import CSV (multipart, single `file` field) and get a validation + planning preview. Nothing is written. Runs row-level, per-handle, and cross-row validation, plans the inserts against current DB state, and HEAD-checks every image URL (only when no other errors exist). Returns aggregate counts plus the full error/warning lists. `can_confirm` is true only when there are no errors. Max 50 MB and 10,000 rows.

POST /admin/api/products/create-import/confirm

Re-upload the same CSV sent to `/create-import/preview` to apply it. The server re-parses and re-validates (the flow is stateless), then inserts all products, variants, categories, and options and fetches images in one pass. On validation failure it returns 422 with the same preview-shaped body (errors populated, created empty) so the UI can re-render. Image fetch failures are non-fatal and reported in `failed_images`. Applying the plan counts against the tenant product cap — exceeding it returns 402 with `error: tier_limit_exceeded`, `limit: max_products`. Max 50 MB and 10,000 rows.

GET /admin/api/products

List products with pagination, filtering, sorting, and search.

Parameters

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 product name
visible string optional Filter by visibility: "true" or "false"
product_type string optional Filter by type: "physical" or "digital"
price_min string optional Minimum price as decimal string (e.g. "10.00")
price_max string optional Maximum price as decimal string (e.g. "50.00")
stock_min integer optional Minimum stock level (inclusive)
stock_max integer optional Maximum stock level (inclusive)
created_from string optional Earliest creation date (inclusive), as YYYY-MM-DD
created_to string optional Latest creation date (inclusive), as YYYY-MM-DD
sort string optional Sort field with optional - prefix for descending. Valid: name, price, stock, created_at. Default: -created_at

Response 200

{
  "data": [
    {
      "id": 1,
      "slug": "classic-cotton-tee",
      "name": "Classic Cotton Tee",
      "price": "29.99",
      "stock": 150,
      "visible": true,
      "created_at": "2025-11-20T14:30:00Z",
      "product_type": "physical",
      "track_stock": true
    },
    {
      "id": 2,
      "slug": "digital-photography-guide",
      "name": "Digital Photography Guide",
      "price": "14.99",
      "stock": 0,
      "visible": true,
      "created_at": "2025-11-19T09:15:00Z",
      "product_type": "digital",
      "track_stock": false
    }
  ],
  "page": 1,
  "per_page": 20,
  "total": 42,
  "total_pages": 3
}

Example

curl "https://yourshop.zeroshop.io/admin/api/products?page=1&per_page=20&search=tee&sort=-price" \
  -H "Authorization: Bearer zspat_..."
POST /admin/api/products

Create a new product. A URL slug is auto-derived from the name. Preview-tier shops are capped at 100 products — exceeding the cap returns 402 with `error: tier_limit_exceeded`, `limit: max_products`. See the Tenant Tier reference.

Request Body

{
  "name": "Classic Cotton Tee",
  "description": "A comfortable 100% cotton t-shirt available in multiple sizes.",
  "price": "29.99",
  "compare_at_price": "39.99",
  "sku": "CCT-001",
  "stock": 150,
  "visible": true,
  "tax_class_id": null,
  "product_type": "physical",
  "track_stock": true,
  "download_limit": null,
  "download_expiry_days": null,
  "is_subscription": false,
  "billing_interval": null,
  "billing_interval_count": null,
  "cancellation_policy": null,
  "max_billing_cycles": null,
  "meta_title": "Classic Cotton Tee — Soft Everyday Basic",
  "meta_description": "A comfortable 100% cotton t-shirt available in multiple sizes.",
  "og_image_media_id": null,
  "translations": [
    {
      "locale": "de",
      "field": "name",
      "value": "Klassisches Baumwoll-T-Shirt"
    },
    {
      "locale": "de",
      "field": "description",
      "value": "Ein bequemes T-Shirt aus 100% Baumwolle."
    }
  ]
}

Response 201

{
  "id": 1,
  "slug": "classic-cotton-tee",
  "name": "Classic Cotton Tee",
  "description": "A comfortable 100% cotton t-shirt available in multiple sizes.",
  "price": "29.99",
  "compare_at_price": "39.99",
  "sku": "CCT-001",
  "stock": 150,
  "visible": true,
  "created_at": "2025-11-20T14:30:00Z",
  "updated_at": "2025-11-20T14:30:00Z",
  "has_variants": false,
  "tax_class_id": null,
  "product_type": "physical",
  "track_stock": true,
  "download_limit": null,
  "download_expiry_days": null,
  "is_subscription": false,
  "billing_interval": null,
  "billing_interval_count": null,
  "cancellation_policy": null,
  "max_billing_cycles": null,
  "translations": {
    "de": {
      "name": "Klassisches Baumwoll-T-Shirt",
      "description": "Ein bequemes T-Shirt aus 100% Baumwolle."
    }
  }
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/products" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Classic Cotton Tee",
    "description": "A comfortable 100% cotton t-shirt.",
    "price": "29.99",
    "compare_at_price": "39.99",
    "sku": "CCT-001",
    "stock": 150,
    "visible": true,
    "product_type": "physical"
  }'
GET /admin/api/products/{id}

Fetch a single product by its numeric ID. Includes content translations.

Response 200

{
  "id": 1,
  "slug": "classic-cotton-tee",
  "name": "Classic Cotton Tee",
  "description": "A comfortable 100% cotton t-shirt available in multiple sizes.",
  "price": "29.99",
  "compare_at_price": "39.99",
  "sku": "CCT-001",
  "stock": 150,
  "visible": true,
  "created_at": "2025-11-20T14:30:00Z",
  "updated_at": "2025-11-22T10:05:00Z",
  "has_variants": true,
  "tax_class_id": null,
  "product_type": "physical",
  "track_stock": true,
  "download_limit": null,
  "download_expiry_days": null,
  "is_subscription": false,
  "billing_interval": null,
  "billing_interval_count": null,
  "cancellation_policy": null,
  "max_billing_cycles": null,
  "translations": {
    "de": {
      "name": "Klassisches Baumwoll-T-Shirt",
      "description": "Ein bequemes T-Shirt aus 100% Baumwolle."
    }
  }
}

Example

curl "https://yourshop.zeroshop.io/admin/api/products/1" \
  -H "Authorization: Bearer zspat_..."
PUT /admin/api/products/{id}

Replace all editable fields of an existing product. Re-derives the slug if the name changes.

Request Body

{
  "name": "Classic Cotton Tee",
  "description": "Updated description for the classic cotton tee.",
  "price": "24.99",
  "compare_at_price": "39.99",
  "sku": "CCT-001",
  "stock": 200,
  "visible": true,
  "tax_class_id": null,
  "product_type": "physical",
  "track_stock": true,
  "download_limit": null,
  "download_expiry_days": null,
  "is_subscription": false,
  "billing_interval": null,
  "billing_interval_count": null,
  "cancellation_policy": null,
  "max_billing_cycles": null,
  "meta_title": "Classic Cotton Tee — Soft Everyday Basic",
  "meta_description": "Updated description for the classic cotton tee.",
  "og_image_media_id": null,
  "quick_order_enabled": true,
  "translations": [
    {
      "locale": "de",
      "field": "name",
      "value": "Klassisches Baumwoll-T-Shirt"
    }
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/1" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Classic Cotton Tee",
    "description": "Updated description.",
    "price": "24.99",
    "compare_at_price": "39.99",
    "sku": "CCT-001",
    "stock": 200,
    "visible": true,
    "product_type": "physical",
    "track_stock": true
  }'
DELETE /admin/api/products/{id}

Soft-delete a product. The row is kept to preserve order history.

Response 204

Example

curl -X DELETE "https://yourshop.zeroshop.io/admin/api/products/1" \
  -H "Authorization: Bearer zspat_..."
GET /admin/api/products/export.csv

Stream a CSV of products and variants matching the current list filters. The editable columns are `price`, `compare_at_price`, and `stock`; everything else is read-only context. `category` is always included; per-option columns are appended when `include_options=true` so the spreadsheet stays searchable by attribute (e.g. Brand=Apple).

Parameters

Name Type Required Description
search string optional Case-insensitive text search on product name
visible string optional Filter by visibility: "true" or "false"
product_type string optional Filter by type: "physical" or "digital"
price_min string optional Minimum price as decimal string (e.g. "10.00")
price_max string optional Maximum price as decimal string (e.g. "50.00")
stock_min integer optional Minimum stock level (inclusive)
stock_max integer optional Maximum stock level (inclusive)
created_from string optional Earliest creation date (inclusive), as YYYY-MM-DD
created_to string optional Latest creation date (inclusive), as YYYY-MM-DD
category_id integer optional Restrict to products assigned to this category id
option_value_id integer optional Restrict to products with this option value (variants or attributes)
include_options boolean optional When true, append one column per option/attribute name (alphabetical). Default false.
sort string optional Sort field with optional - prefix for descending. Valid: name, price, stock, created_at. Default: -created_at

Response 200

"kind,id,product_id,product_name,variant_label,sku,price,compare_at_price,stock,updated_at,category\nproduct,1,1,Classic Cotton Tee,,TEE-001,29.99,34.99,150,2025-11-20T14:30:00Z,Apparel\nvariant,7,2,Merino Wool Beanie,Charcoal / M,BEANIE-CHR-M,24.99,,42,2025-11-21T08:15:00Z,Accessories\nvariant,8,2,Merino Wool Beanie,Charcoal / L,BEANIE-CHR-L,24.99,,38,2025-11-21T08:15:00Z,Accessories\n"

Example

curl "https://yourshop.zeroshop.io/admin/api/products/export.csv?include_options=true" \
  -H "Authorization: Bearer zspat_..." \
  -o products-export.csv
POST /admin/api/products/import/preview

Upload a CSV (multipart, single `file` field) and get a parsed preview of the changes that would be applied. Nothing is written. Re-uploading the same file to `/import/confirm` performs the writes. The CSV must include the required columns: kind, id, product_id, product_name, variant_label, sku, price, compare_at_price, stock, updated_at. Only `price`, `compare_at_price`, and `stock` are editable; other columns are matched for identity or treated as read-only context. Auto-detects `,`, `;`, or tab delimiters. Max 20 MB and 100,000 rows.

Request Body

{
  "file": "(CSV file with the header described above; up to 20 MB)"
}

Response 200

{
  "summary": {
    "total_rows": 3,
    "changed": 2,
    "unchanged": 1,
    "warnings": 1,
    "errors": 0,
    "skippable_error_rows": 0,
    "valid_change_rows": 2
  },
  "changes": [
    {
      "row": 2,
      "kind": "product",
      "id": 1,
      "product_name": "Classic Cotton Tee",
      "variant_label": "",
      "fields": {
        "price": {
          "old": "29.99",
          "new": "27.99"
        },
        "compare_at_price": null,
        "stock": {
          "old": 150,
          "new": 180
        }
      },
      "warnings": []
    },
    {
      "row": 3,
      "kind": "variant",
      "id": 7,
      "product_name": "Merino Wool Beanie",
      "variant_label": "Charcoal / M",
      "fields": {
        "price": null,
        "compare_at_price": null,
        "stock": {
          "old": 42,
          "new": 30
        }
      },
      "warnings": [
        "product_name in CSV differs from DB; ignored"
      ]
    }
  ],
  "errors": [],
  "stale_rows": [],
  "can_confirm": true
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/products/import/preview" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@products-edited.csv"
POST /admin/api/products/import/confirm

Re-upload the same CSV that was sent to `/import/preview` to apply the changes. The server re-parses, re-validates, and writes all updates in a single transaction so DB drift between preview and confirm is caught. By default any row-level error returns 422 with the full error list; pass `skip_invalid_rows=true` to drop invalid rows and apply the rest. File-level errors (parse, missing column, oversized) always return 422.

Parameters

Name Type Required Description
skip_invalid_rows boolean optional If true, drop rows with row-level errors and apply the rest. Default false (any error blocks the import).

Request Body

{
  "file": "(same CSV uploaded to /import/preview; up to 20 MB)"
}

Response 200

{
  "applied": 2,
  "products_touched": 1,
  "variants_touched": 1,
  "skipped": 0,
  "skipped_rows": []
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/products/import/confirm" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@products-edited.csv"
GET /admin/api/products/{id}/variants

Get the options and variants for a product. Options are tenant-level; variants are product-level combinations.

Response 200

{
  "options": [
    {
      "id": 1,
      "name": "Size",
      "values": [
        {
          "id": 10,
          "value": "S"
        },
        {
          "id": 11,
          "value": "M"
        },
        {
          "id": 12,
          "value": "L"
        }
      ]
    },
    {
      "id": 2,
      "name": "Color",
      "values": [
        {
          "id": 20,
          "value": "Red"
        },
        {
          "id": 21,
          "value": "Blue"
        }
      ]
    }
  ],
  "variants": [
    {
      "id": 100,
      "slug": "classic-cotton-tee-s-red",
      "sku": "CCT-S-RED",
      "price": "29.99",
      "compare_at_price": "39.99",
      "stock": 25,
      "visible": true,
      "option_value_ids": [
        10,
        20
      ],
      "label": "S / Red",
      "weight_grams": 200,
      "length_cm": 30.0,
      "width_cm": 25.0,
      "height_cm": 2.0
    },
    {
      "id": 101,
      "slug": "classic-cotton-tee-m-blue",
      "sku": "CCT-M-BLU",
      "price": "29.99",
      "compare_at_price": null,
      "stock": 40,
      "visible": true,
      "option_value_ids": [
        11,
        21
      ],
      "label": "M / Blue",
      "weight_grams": 220,
      "length_cm": 32.0,
      "width_cm": 26.0,
      "height_cm": 2.0
    }
  ]
}

Example

curl "https://yourshop.zeroshop.io/admin/api/products/1/variants" \
  -H "Authorization: Bearer zspat_..."
PUT /admin/api/products/{id}/variants

Replace all options and variants for a product. Each variant must specify a value for every option.

Request Body

{
  "option_ids": [
    1,
    2
  ],
  "variants": [
    {
      "slug": "classic-cotton-tee-s-red",
      "sku": "CCT-S-RED",
      "price": "29.99",
      "compare_at_price": "39.99",
      "stock": 25,
      "visible": true,
      "option_value_ids": [
        10,
        20
      ],
      "weight_grams": 200,
      "length_cm": 30.0,
      "width_cm": 25.0,
      "height_cm": 2.0
    },
    {
      "slug": "classic-cotton-tee-m-blue",
      "sku": "CCT-M-BLU",
      "price": "29.99",
      "compare_at_price": null,
      "stock": 40,
      "visible": true,
      "option_value_ids": [
        11,
        21
      ],
      "weight_grams": 220,
      "length_cm": 32.0,
      "width_cm": 26.0,
      "height_cm": 2.0
    }
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/1/variants" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "option_ids": [1, 2],
    "variants": [
      {
        "sku": "CCT-S-RED",
        "price": "29.99",
        "stock": 25,
        "visible": true,
        "option_value_ids": [10, 20],
        "weight_grams": 200
      }
    ]
  }'
GET /admin/api/products/{id}/attributes

Get the attribute values currently assigned to a product. Returns option/value pairs grouped by attribute.

Response 200

[
  {
    "option_id": 5,
    "option_name": "Material",
    "values": [
      {
        "id": 50,
        "value": "Cotton"
      },
      {
        "id": 51,
        "value": "Polyester"
      }
    ]
  },
  {
    "option_id": 6,
    "option_name": "Season",
    "values": [
      {
        "id": 60,
        "value": "Summer"
      }
    ]
  }
]

Example

curl "https://yourshop.zeroshop.io/admin/api/products/1/attributes" \
  -H "Authorization: Bearer zspat_..."
PUT /admin/api/products/{id}/attributes

Replace all attribute values for a product. Each referenced option must have option_type 'attribute'.

Request Body

{
  "attributes": [
    {
      "option_id": 5,
      "option_value_ids": [
        50,
        51
      ]
    },
    {
      "option_id": 6,
      "option_value_ids": [
        60
      ]
    }
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/1/attributes" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "attributes": [
      { "option_id": 5, "option_value_ids": [50, 51] },
      { "option_id": 6, "option_value_ids": [60] }
    ]
  }'
GET /admin/api/products/{id}/images

List all images for a product, ordered by sort_order.

Response 200

{
  "images": [
    {
      "id": 1,
      "storage_key": "t_abc123/products/1/550e8400-e29b-41d4-a716-446655440000.webp",
      "url": "https://cdn.zeroshop.io/t_abc123/products/1/550e8400-e29b-41d4-a716-446655440000.webp",
      "alt_text": "",
      "sort_order": 0
    },
    {
      "id": 2,
      "storage_key": "t_abc123/products/1/6ba7b810-9dad-11d1-80b4-00c04fd430c8.webp",
      "url": "https://cdn.zeroshop.io/t_abc123/products/1/6ba7b810-9dad-11d1-80b4-00c04fd430c8.webp",
      "alt_text": "Back view",
      "sort_order": 1
    }
  ]
}

Example

curl "https://yourshop.zeroshop.io/admin/api/products/1/images" \
  -H "Authorization: Bearer zspat_..."
POST /admin/api/products/{id}/images

Upload a product image via multipart form. The image is resized to max 1200x1200px and re-encoded as WebP.

Request Body

{
  "file": "(binary image data — JPEG, PNG, GIF, or WebP)"
}

Response 201

{
  "id": 3,
  "storage_key": "t_abc123/products/1/a1b2c3d4-e5f6-7890-abcd-ef1234567890.webp",
  "url": "https://cdn.zeroshop.io/t_abc123/products/1/a1b2c3d4-e5f6-7890-abcd-ef1234567890.webp",
  "alt_text": "",
  "sort_order": 2
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/products/1/images" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@product-photo.jpg"
PUT /admin/api/products/{id}/images/reorder

Reorder images for a product. All image IDs must belong to the product.

Request Body

{
  "ordered_ids": [
    2,
    1,
    3
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/1/images/reorder" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{"ordered_ids": [2, 1, 3]}'
DELETE /admin/api/products/{id}/images/{img_id}

Delete a product image. Removes the record and the file from object storage.

Response 204

Example

curl -X DELETE "https://yourshop.zeroshop.io/admin/api/products/1/images/3" \
  -H "Authorization: Bearer zspat_..."
GET /admin/api/files

List every digital file in the tenant's library.

Response 200

{
  "files": [
    {
      "id": 1,
      "original_filename": "photography-guide-v2.pdf",
      "file_size": 5242880,
      "content_type": "application/pdf",
      "sort_order": 0,
      "created_at": "2025-11-20T14:30:00Z"
    },
    {
      "id": 2,
      "original_filename": "bonus-presets.zip",
      "file_size": 15728640,
      "content_type": "application/zip",
      "sort_order": 1,
      "created_at": "2025-11-21T09:00:00Z"
    }
  ]
}

Example

curl "https://yourshop.zeroshop.io/admin/api/files" \
  -H "Authorization: Bearer zspat_..."
POST /admin/api/files

Upload a new digital file via multipart form (single `file` field). Files are stored as-is (no processing) and added to the tenant's flat file library. To make a file available to customers, attach it to one or more access groups via PUT /admin/api/access-groups/{id}/files. Tier caps are enforced — exceeding the per-file or aggregate digital-bytes cap returns 402 with `error: tier_limit_exceeded`. The request body limit is 128 MiB.

Request Body

{
  "file": "(binary file data — any format)"
}

Response 201

{
  "id": 3,
  "original_filename": "extra-resources.zip",
  "file_size": 10485760,
  "content_type": "application/zip",
  "sort_order": 0,
  "created_at": ""
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/files" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@photography-guide.pdf"
DELETE /admin/api/files/{file_id}

Delete a digital file from the library. Removes the record and the underlying object from storage. Cascades to remove the file from any access groups it was attached to.

Response 204

Example

curl -X DELETE "https://yourshop.zeroshop.io/admin/api/files/3" \
  -H "Authorization: Bearer zspat_..."
GET /admin/api/products/{id}/recommendations

List all cross-sell and upsell recommendation links for a product.

Response 200

[
  {
    "id": 1,
    "source_product_id": 1,
    "target_product_id": 5,
    "recommendation_type": "cross_sell",
    "sort_order": 0
  },
  {
    "id": 2,
    "source_product_id": 1,
    "target_product_id": 8,
    "recommendation_type": "cross_sell",
    "sort_order": 1
  },
  {
    "id": 3,
    "source_product_id": 1,
    "target_product_id": 12,
    "recommendation_type": "upsell",
    "sort_order": 0
  }
]

Example

curl "https://yourshop.zeroshop.io/admin/api/products/1/recommendations" \
  -H "Authorization: Bearer zspat_..."
PUT /admin/api/products/{id}/recommendations

Replace all recommendations of a given type for a product. Pass an empty target list to clear that type.

Request Body

{
  "recommendation_type": "cross_sell",
  "target_product_ids": [
    5,
    8,
    15
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/1/recommendations" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "recommendation_type": "cross_sell",
    "target_product_ids": [5, 8, 15]
  }'
DELETE /admin/api/products/{id}/recommendations/{rec_id}

Delete a single recommendation link by its ID.

Response 204

Example

curl -X DELETE "https://yourshop.zeroshop.io/admin/api/products/1/recommendations/3" \
  -H "Authorization: Bearer zspat_..."
GET /admin/api/products/{id}/bundle-items

List all bundle components for a bundle product.

Response 200

[
  {
    "id": 1,
    "bundle_product_id": 10,
    "component_product_id": 1,
    "component_variant_id": 100,
    "quantity": 1,
    "sort_order": 0
  },
  {
    "id": 2,
    "bundle_product_id": 10,
    "component_product_id": 3,
    "component_variant_id": 300,
    "quantity": 2,
    "sort_order": 1
  }
]

Example

curl "https://yourshop.zeroshop.io/admin/api/products/10/bundle-items" \
  -H "Authorization: Bearer zspat_..."
PUT /admin/api/products/{id}/bundle-items

Replace all bundle components for a product. Pass an empty items list to clear the bundle.

Request Body

{
  "items": [
    {
      "product_id": 1,
      "variant_id": 100,
      "quantity": 1
    },
    {
      "product_id": 3,
      "variant_id": 300,
      "quantity": 2
    }
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/10/bundle-items" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "product_id": 1, "variant_id": 100, "quantity": 1 },
      { "product_id": 3, "variant_id": 300, "quantity": 2 }
    ]
  }'
GET /admin/api/products/{id}/custom-fields

Get the custom fields currently linked to a product.

Response 200

[
  {
    "id": 1,
    "name": "Washing Instructions",
    "field_type": "text",
    "value": "Machine wash cold, tumble dry low"
  },
  {
    "id": 2,
    "name": "Country of Origin",
    "field_type": "choice",
    "value": "Portugal"
  }
]

Example

curl "https://yourshop.zeroshop.io/admin/api/products/1/custom-fields" \
  -H "Authorization: Bearer zspat_..."
PUT /admin/api/products/{id}/custom-fields

Replace the set of custom fields linked to a product. Pass custom field IDs in display order.

Request Body

{
  "custom_field_ids": [
    1,
    2,
    5
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/1/custom-fields" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{"custom_field_ids": [1, 2, 5]}'
GET /admin/api/products/{id}/categories

Get the category IDs currently assigned to a product.

Response 200

{
  "category_ids": [
    3,
    7,
    12
  ]
}

Example

curl "https://yourshop.zeroshop.io/admin/api/products/1/categories" \
  -H "Authorization: Bearer zspat_..."
PUT /admin/api/products/{id}/categories

Replace all category assignments for a product. Pass an empty list to clear all assignments.

Request Body

{
  "category_ids": [
    3,
    7,
    12
  ]
}

Response 204

Example

curl -X PUT "https://yourshop.zeroshop.io/admin/api/products/1/categories" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{"category_ids": [3, 7, 12]}'
PATCH /admin/api/products/bulk

Apply the same set of boolean field overrides to many products in a single round trip. Pass the product `ids` (1–500) and a `changes` object; only the `changes` fields you set are written, the rest are left untouched. At least one `changes` field must be present. Soft-deleted rows in `ids` are silently ignored. Returns the count of distinct live products that matched.

Request Body

{
  "ids": [
    1,
    2,
    3
  ],
  "changes": {
    "visible": true,
    "track_stock": false,
    "requires_delivery_date": false,
    "quick_order_enabled": true
  }
}

Response 200

{
  "updated": 3
}

Example

curl -X PATCH "https://yourshop.zeroshop.io/admin/api/products/bulk" \
  -H "Authorization: Bearer zspat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [1, 2, 3],
    "changes": { "visible": true, "quick_order_enabled": true }
  }'
POST /admin/api/products/{id}/clone

Deep-clone an existing product, including its images (each image is server-side-copied to a fresh object-storage key under the new product so deletes on either side stay isolated). The clone is hidden by default. Counts against the tenant product cap — exceeding it returns 402 with `error: tier_limit_exceeded`, `limit: max_products`. Returns the new product's id and slug.

Response 201

{
  "id": 43,
  "slug": "classic-cotton-tee-copy"
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/products/1/clone" \
  -H "Authorization: Bearer zspat_..."
GET /admin/api/products/create-import/example.csv

Download a filled-in example CSV template that demonstrates all 39 supported columns (Handle, Name, Description, Product Type, Price, variants, images, categories, …) with two example products. Use it as the starting point for a create-import.

Response 200

"Handle,Name,Description,Product Type,Visible,Price,Compare At Price,SKU,Stock,Track Stock,Tax Class,Weight,Weight Unit,Length (cm),Width (cm),Height (cm),Requires Delivery Date,Categories,Cross Sell Handles,Image Src,Image Position,Image Alt Text,Option1 Name,Option1 Value,Option2 Name,Option2 Value,Option3 Name,Option3 Value,Variant SKU,Variant Price,Variant Compare At Price,Variant Inventory Qty,Variant Weight,Variant Weight Unit,Variant Length (cm),Variant Width (cm),Variant Height (cm),Variant Image,Variant Visible\nsimple-mug,Simple Mug,\"A plain ceramic mug, dishwasher safe.\",physical,true,9.99,,MUG-1,50,true,,300,g,,,,,Home > Kitchen,,https://placehold.co/600x600/png,1,Simple mug photo,,,,,,,,,,,,,,,,\nclassic-tee,Classic Tee,\"100% cotton unisex tee.\",physical,true,19.99,24.99,,,true,,200,g,,,,,Men > Apparel | Sale,simple-mug,https://placehold.co/600x600/png?text=Tee+Front,1,Tee front,Size,S,Color,Red,,TEE-S-RED,19.99,,12,,,,,https://placehold.co/600x600/png?text=Tee+Front,true\n"

Example

curl "https://yourshop.zeroshop.io/admin/api/products/create-import/example.csv" \
  -H "Authorization: Bearer zspat_..." \
  -o product-create-example.csv
POST /admin/api/products/create-import/preview

Upload a create-import CSV (multipart, single `file` field) and get a validation + planning preview. Nothing is written. Runs row-level, per-handle, and cross-row validation, plans the inserts against current DB state, and HEAD-checks every image URL (only when no other errors exist). Returns aggregate counts plus the full error/warning lists. `can_confirm` is true only when there are no errors. Max 50 MB and 10,000 rows.

Parameters

Name Type Required Description
duplicate_mode string optional How to treat handles that collide with existing live products. Sent as a multipart field, not a query param. "replace" soft-deletes the live product and recreates it; anything else (or absent) skips the duplicate. Default: skip.

Request Body

{
  "file": "(create-import CSV with the 39-column header; up to 50 MB)"
}

Response 200

{
  "summary": {
    "products_to_create": 2,
    "variants_to_create": 3,
    "images_to_fetch": 3,
    "categories_to_create": [
      "Home > Kitchen",
      "Men > Apparel"
    ],
    "options_to_create": [
      "Size",
      "Color"
    ]
  },
  "errors": [],
  "warnings": [
    {
      "row": 4,
      "message": "category \"Sale\" will be auto-created"
    }
  ],
  "can_confirm": true
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/products/create-import/preview" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@products-to-create.csv" \
  -F "duplicate_mode=skip"
POST /admin/api/products/create-import/confirm

Re-upload the same CSV sent to `/create-import/preview` to apply it. The server re-parses and re-validates (the flow is stateless), then inserts all products, variants, categories, and options and fetches images in one pass. On validation failure it returns 422 with the same preview-shaped body (errors populated, created empty) so the UI can re-render. Image fetch failures are non-fatal and reported in `failed_images`. Applying the plan counts against the tenant product cap — exceeding it returns 402 with `error: tier_limit_exceeded`, `limit: max_products`. Max 50 MB and 10,000 rows.

Parameters

Name Type Required Description
duplicate_mode string optional Same as on /create-import/preview, sent as a multipart field. "replace" or skip (default).

Request Body

{
  "file": "(same CSV uploaded to /create-import/preview; up to 50 MB)"
}

Response 200

{
  "created": [
    {
      "id": 42,
      "handle": "simple-mug"
    },
    {
      "id": 43,
      "handle": "classic-tee"
    }
  ],
  "failed_images": [
    {
      "product_id": 43,
      "url": "https://placehold.co/600x600/png?text=Tee+Back",
      "reason": "fetch failed: 404 Not Found"
    }
  ],
  "errors": [],
  "warnings": []
}

Example

curl -X POST "https://yourshop.zeroshop.io/admin/api/products/create-import/confirm" \
  -H "Authorization: Bearer zspat_..." \
  -F "file=@products-to-create.csv" \
  -F "duplicate_mode=skip"

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.