# Create and update products

> Create, update, publish, unpublish and delete products in a Shoplazza store with the Open API, and see why a new product starts life as a draft.

Create products with the Open API, update their fields, publish or unpublish them, and delete them. A product created here starts as a draft and becomes visible on the storefront when you publish it.

## When to use this

Publishing new products from your app, bulk importing a catalog, or building a product management back office.

## Prerequisites

- A working public app with OAuth and token storage — follow [Develop a standalone app](/docs/app/getting-started/create-public-app/develop-app) first. Every request below carries an `Access-Token: {token}` header, using the token you saved for each shop.
- The `product` access scope granted during OAuth.

## How it works

A product is built with a single call: `options`, `variants`, and `images` are all nested inside the same request.

- For a product with options (like size or color), set `has_only_default_variant: false`, declare `options`, and give each variant an `option1` value that matches — plus `need_variant_image: false`.
- If `inventory_tracking: true`, `inventory_policy` is also required.
- `images` must contain at least one image.
- The product starts as a **draft**. Nothing is visible on the storefront until you set `published: true` (or schedule it with `auto_publish_at`).

### Create a product

A product is created in one call: the request nests everything under `product`. `title`, `has_only_default_variant`, `images`, and `variants` are required, and each variant needs a `price`.

`POST /openapi/2026-01/products`

Request body:

```json5
{
  "product": {
    "title": "Summer cotton T-shirt",
    "published": false,          // start as a draft; publish later
    "has_only_default_variant": false,
    "need_variant_image": false, // required when has_only_default_variant is false
    "inventory_tracking": true,
    "inventory_policy": "deny",  // required when inventory_tracking is true
    "options": [{ "name": "Size", "values": ["S", "M"] }],
    "images": [{ "src": "https://cdn.example.com/tshirt.jpg" }], // at least one image
    "variants": [
      { "option1": "S", "price": 19.9, "sku": "TS-S", "inventory_quantity": 100, "position": 1 },
      { "option1": "M", "price": 19.9, "sku": "TS-M", "inventory_quantity": 100, "position": 2 }
    ]
  }
}
```

Response (excerpt, created product under `data.data.product`):

```json
{
  "code": "Success",
  "data": {
    "product": {
      "id": "4d3e7859-455c-4899-a296-7befe731c585",
      "title": "Summer cotton T-shirt",
      "handle": "summer-cotton-t-shirt",
      "inventory_tracking": true,
      "inventory_policy": "deny",
      "options": [
        { "id": "bca26b9d-c3f1-48ac-93aa-e4a975c44dce", "name": "Size", "values": ["S", "M"], "position": 1 }
      ],
      "variants": [
        { "id": "28ea5ee4-5b1d-4a9d-a92e-e60f0dff3da8", "title": "S", "option1": "S", "position": 1, "price": 19.9, "sku": "TS-S", "inventory_quantity": 100 }
      ],
      "price_min": 19.9,
      "price_max": 19.9,
      "created_at": "2026-07-02T02:57:25Z",
      "updated_at": "2026-07-02T02:57:25Z"
    }
  }
}
```

:::note
`images` must contain at least one item — an empty array is rejected with `value must contain at least 1 item(s)`. Host the image somewhere public and pass its URL as `src`. See [Create product](/api/product-create) for the full request body.
:::

### Get a product

Fetch one product by id.

`GET /openapi/2026-01/products/{id}`

Response (excerpt): the full product object, including `options[]`, `images[]`, `variants[]`, and `primary_image`, alongside `id`, `title`, `handle`, `inventory_tracking`, `inventory_policy`, `price_min`, `price_max`, `created_at`, `updated_at`.

Full fields: see [Get product](/api/product-detail).

### Update a product

Change product-level fields (title, description, tags, and so on) with `PUT`. Only the fields you send are changed.

`PUT /openapi/2026-01/products/{id}`

Request body:

```json5
{
  "product": {
    "title": "Summer cotton T-shirt (2026)" // only the fields you want to change
  }
}
```

Returns the updated product. Editable fields are listed in the [Update product](/api/product-update) reference.

### Publish and unpublish

Publishing is just the `published` flag. Set it to `true` to make the product visible on the storefront, `false` to hide it again — the same `PUT` endpoint, both directions.

`PUT /openapi/2026-01/products/{id}`

Request body:

```json5
{
  "product": {
    "published": true // false to unpublish
  }
}
```

Response (excerpt, same product shape as create, with `published_at` now set):

```json
{
  "code": "Success",
  "data": {
    "product": {
      "id": "4d3e7859-455c-4899-a296-7befe731c585",
      "title": "Summer cotton T-shirt",
      "published": true,
      "published_at": "2026-07-02T02:58:55Z"
    }
  }
}
```

After publishing, the product is reachable on the storefront at `https://<shop>/products/<handle>`.

:::note
To publish on a schedule, set `auto_publish_at` (RFC 3339) when creating or updating. To take a product offline automatically when it sells out, set `inventory_policy: auto_unpublished`. See [Create product](/api/product-create).
:::

### Delete a product

`DELETE /openapi/2026-01/products/{id}`

Response:

```json
{ "code": "Success", "data": {} }
```

See [Delete product](/api/product-delete).

To delete many at once, use `DELETE /products?product_ids=id1,id2` — see [Batch delete products](/api/product-batch-delete).

### List products

The list endpoint uses cursor pagination (`cursor` plus `per_page`, max 250) and filters such as `title`, `collection_id`, and `published_status`.

`GET /openapi/2026-01/products`

Response (excerpt, list under `data.data.products`, cursor under `data.data.cursor`):

```json
{
  "code": "Success",
  "data": {
    "cursor": "MjAyNi0wMS0yM1QxMDowODoyN1o...",
    "products": [
      {
        "id": "ac7783d3-df2d-47fe-abca-7ee3d9bceb04",
        "title": "T-shirt",
        "handle": "t-shirt",
        "inventory_quantity": 666,
        "price_min": 11,
        "price_max": 33,
        "created_at": "2026-07-01T09:25:50Z"
      }
    ]
  }
}
```

Full filters: see [List products](/api/products).

To get only the total count, use `GET /products/count` — see [Count products](/api/product-count).

## Next steps

- [Manage variants and images](/docs/app/use-cases/product-merchandising/manage-variants-and-images)
- [Organize products into collections](/docs/app/use-cases/product-merchandising/organize-collections)
