# Create or update a product

`POST /api/bobsync/products`

**Base URLs:**

- Sandbox: `https://api.tools.qa.bobshop.co.za`
- Production: `https://api.tools.bobshop.co.za`

**Authentication:** Bearer token required (see the Authentication page).

## Example request

```bash
curl -X POST 'https://api.tools.qa.bobshop.co.za/api/bobsync/products' \
  -H "content-type: application/json" \
  -H "Authorization: Bearer <API_KEY>" \
  -d '{
  "product_reference": "abc123",
  "name": "Demo Product",
  "description": "This is a product",
  "condition": "NEW",
  "delivery_type": "STANDARD_SHIPPING",
  "discreet_listing": false,
  "gtin": null,
  "handling_time": 3,
  "quantity_per_lot": 1,
  "tags": "electronics,gadgets",
  "vendor": "ACME Inc.",
  "images": [
    {
      "src": "https://example.com/image1.jpg",
      "position": 1
    }
  ],
  "variants": [
    {
      "variant_id_text": "ab1",
      "sku": "SKU-ABC123",
      "barcode": "123456789012",
      "image_src": "https://example.com/image1.jpg",
      "position": 1,
      "price": 199.99,
      "price_compare": 249.99,
      "quantity": 5,
      "weight": 1.23
    }
  ]
}'
```

Send the product without an `id` to create it. If the `product_reference`
already exists on your channel, that product is updated instead. To update,
send the same `product_reference` again, or our `id`.

The response is the product as stored, including the `id` we assigned.

- A `200` means we stored the product, not that a listing exists. Your
  channel needs a category rule first. See [What makes a product go live](../4_What%20makes%20a%20product%20go%20live/README.md).
- The `variants` array replaces the product's variants. A variant you leave
  out is deleted. Always send the complete list. Do not send an empty list or
  leave `variants` out: today that deletes every variant but leaves the
  listing live with its old stock. To take a product down, use
  [Delete a product](product_delete.md).
- `id` values are per-environment and take precedence over
  `product_reference`. Only send one you are certain of. See [Important concepts](../5_Important%20concepts/README.md).

## Product fields

`condition`, `delivery_type`, `discreet_listing`, `handling_time` and
`quantity_per_lot` do not reach your listings today. Only rules on your channel
set them. Add the rules in Bob Sync, on your channel's Rules page. `gtin`
cannot be set at all today.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `id` | integer | no | Our key, assigned on create. Per-environment. Takes precedence over `product_reference`. |
| `product_reference` | string | yes | Your reference for the product. Unique within your channel and stable. |
| `name` | string | yes | The listing title. Required to list. |
| `description` | string | yes | Cut off at 7,500 characters. HTML is allowed; scripts, embeds, iframes, objects and forms are stripped. |
| `condition` | string | no | `NEW`, `SECOND_HAND`, `REFURBISHED` or `OPEN_BOX`. Set only by a channel rule today, not by this field. |
| `delivery_type` | string | no | `STANDARD_SHIPPING`, `DIGITAL_DELIVERY`, `ALLOWS_COLLECTION` or `COLLECTION_ONLY`. Set only by a channel rule today, not by this field. |
| `discreet_listing` | boolean | no | Hides the buyer's user name. Defaults to `false`. Set only by a channel rule today, not by this field. |
| `gtin` | string | no | Global Trade Item Number. **Not currently carried through to listings.** Defaults to `null`. |
| `handling_time` | integer | no | Days to have an order ready for collection, at most 15. Set only by a channel rule today, not by this field. |
| `quantity_per_lot` | integer | no | Minimum purchase quantity. Defaults to 1. Set only by a channel rule today, not by this field. |
| `tags` | string | no | Comma-separated tags for the listing. |
| `vendor` | string | no | The vendor of the product. |
| `images` | array of ProductImage | yes, to list | A product with no images cannot become a listing. |
| `variants` | array of Variant | yes, to list | At least one. **Replaces the product's variants; a variant left out is deleted.** |

## ProductImage fields

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `src` | string | yes | An absolute URL. |
| `position` | integer | no | Sort order, ascending. |

## Variant fields

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `variant_id_text` | string | yes | Your reference for the variant, unique within the product. An unrecognised value creates a new variant. |
| `sku` | string | recommended | Shown on the listing and returned as `tradersReference` on order webhooks. Your key for matching a sale to your stock. |
| `price` | number | yes | Must be above zero to list. |
| `quantity` | integer | yes | At least 1 and below 100,000 to list. |
| `weight` | number | recommended | Kilograms. Used to estimate shipping at checkout. |
| `price_compare` | number | no | Shown struck through, only when greater than `price`. |
| `image_src` | string | no | The variant's image. If the URL is also in `images`, the listing switches to it when the variant is selected. |
| `barcode` | string | no | Barcode data. Stored but not currently displayed. |
| `position` | integer | no | Sort order, ascending. |
| `name` | string | no | On a product with one variant, added to the listing title as "product name - variant name". Not displayed on a product with two or more variants. |
| `description` | string | no | On a product with one variant, replaces the product description when not empty. Not displayed on a product with two or more variants. |
| `options` | array of Option | conditional | Size, colour and so on. **Required when the product has two or more variants.** See [Important concepts](../5_Important%20concepts/README.md). |

## Validation

This endpoint does not currently check `product_reference`, `condition`,
`delivery_type` or `handling_time`. A missing reference or an unrecognised
value is accepted with a `200`. [Bulk upload](../7_Bulk%20upload/README.md) does check them. Send valid values
on both.

## Listing-detail fields in the response

`condition`, `delivery_type`, `discreet_listing`, `gtin`, `handling_time` and
`quantity_per_lot` are not stored on the product, so the response shows:

| Call | What the response shows |
| --- | --- |
| Create | Empty: `""`, `0`, `false` or `null`. |
| Update | The values you sent. |
| [Fetch products](product_fetch.md) | Empty, always. |

An empty value does not mean the request was rejected.

## Failures

| Status | Body | Meaning |
| --- | --- | --- |
| `400` | `failed to JSON decode the request body ...` | The body is not valid JSON. |
| `404` | `failed to get product: ...` | The product lookup failed on our side. Retry. |
| `413` | `{"message":"Request Entity Too Large"}`, or an HTML page on Sandbox | The body is over 1 MB on Sandbox or 6 MB on Production. |
| `429` | `... is busy. Will retry...` | The same product is already being processed. Retry shortly. |
| `500` | `product does not belong to given channel` | The `id` belongs to a product on another channel. |
| `500` | `handler failed, because handler function crashed: ...` | The `id` does not exist on this environment. See [Important concepts](../5_Important%20concepts/README.md). |
| `500` | `failed to insert product`, `failed to update product` or `failed to update product variants` | A write failed on our side. Retry. |

A `500` with either of the first two messages is caused by the request. Check
the `id` you sent before retrying.

## Response

`200` with the stored product, including the `id` we assigned. The example
is a create, so the listing-detail fields are empty.

```json
{
    "id": 486875,
    "product_reference": "abc123",
    "name": "Demo Product",
    "description": "This is a product",
    "condition": "",
    "delivery_type": "",
    "discreet_listing": false,
    "gtin": null,
    "handling_time": 0,
    "quantity_per_lot": 0,
    "tags": "electronics,gadgets",
    "vendor": "ACME Inc.",
    "images": [
        { "src": "https://example.com/image1.jpg", "position": 1 }
    ],
    "variants": [
        {
            "variant_id_text": "ab1",
            "sku": "SKU-ABC123",
            "barcode": "123456789012",
            "description": "",
            "image_src": "https://example.com/image1.jpg",
            "name": "",
            "position": 1,
            "price": 199.99,
            "price_compare": 249.99,
            "quantity": 5,
            "weight": 1.23,
            "options": null
        }
    ]
}
```
