---
title: Product Create / Update
description: "The product update API allows you to send bulk catalog updates to Releva. Any catalog updates submitted through this API will be live within 10 seconds of receiving a successful response. ## Do I need to call it? Yes, you…"
url: "https://releva.ai/documentation/catalog/product-update/"
updated: "2026-09-22"
category: Developer Documentation / Catalog
---

# Product Create / Update

The product update API allows you to send bulk catalog updates to Releva. Any catalog updates submitted through this API will be live within 10 seconds of receiving a successful response.

## Do I need to call it?

Yes, you need to call it whenever changes occur in your catalog — for example when a product (or products) becomes out of stock, or in stock, or the price changes. This is the primary method Releva uses to learn about changes in your catalogue.

## Endpoint

| **URL** | `POST https://releva.ai/api/v0/products` |
| --- | --- |
| **Authentication** | `Authorization: Bearer <secretKey>` — the secret key of the domain whose catalog you are updating. |
| **Content type** | `application/json` |
| **Max request body** | 800 KB. Split larger catalogs into several requests (a few hundred products per request is a good batch size). |

## Request Parameters

You may perform requests to the API using the following curl equivalent:

```
curl -H 'Content-Type: application/json' -H 'Authorization: Bearer <secretKey>' -XPOST https://releva.ai/api/v0/products -d'{
  "products": [
    {
      "id": "productId1_en_uk",
      "name": "Test Product",
      "description": "Some Description",
      "locale": "en",
      "currency": "USD",
      "listPrice": 12.5,
      "discountPrice": 10.99,
      "stockQuantity": 42,
      "sku": "TP-001",
      "categories": [
        "men/shoes/hiking"
      ],
      "available": true,
      "url": "https://my-awesome-shop/uk/products/productId1",
      "canonicalUrl": "https://my-awesome-shop/products/productId1",
      "imageUrl": "https://cdn.my-awesome-shop/products/productId1_img1.png",
      "publishedAt": "2019-06-19T23:45:56Z",
      "marketHandle": "uk",
      "markets": ["GB"],
      "data": {"badge": "New in"},
      "custom": {
        "string": [
          {"key": "color", "values": ["red", "blue"]}
        ],
        "numeric": [
          {"key": "size", "values": [14, 15, 18]}
        ],
        "date": [
           {"key": "in_promo_until", "values": ["2022-11-01T00:00:00.000Z"]}
        ]
      },
      "inventory": [
        {
          "location": [
             {"lat": 39.9352959, "lon": 23.5825895}
          ]
        }
      ]
    }
  ]
}'
```

**Note:** `publishedAt` must include the time *and* a timezone offset (`2019-06-19T23:45:56Z`, `2019-06-19T23:45:56.000Z` or `2019-06-19T23:45:56+0300`). A bare date or a date-time without an offset is rejected with HTTP 400.

## What if my products have variants?

Send one product object per purchasable page, and describe its variants inline in the `variants` array. Releva keeps the product-level information (name, description, categories, URL) on the product itself and stores the per-variant attributes (price, availability, stock, SKU, barcode, image, options such as size and colour) in `variants`.

Two fields work together here:

- `groupId` — the id of the product all these entries belong to. Use the same `groupId` for every document that represents the same product (for example the same product in another language or another market).
- `variants` — the per-variant data.

**Send them together.** A `groupId` without a non-empty `variants` array is ignored, and a product sent without `variants` is automatically treated as a single-variant product whose group is the product itself.

```
curl -H 'Content-Type: application/json' -H 'Authorization: Bearer <secretKey>' -XPOST https://releva.ai/api/v0/products -d'{
  "products": [
    {
      "id": "productId1_en_uk",
      "name": "Test Product",
      "description": "Some Description",
      "locale": "en",
      "currency": "GBP",
      "listPrice": 12.5,
      "discountPrice": 10.99,
      "stockQuantity": 7,
      "sku": "TP-001-S-RED",
      "categories": [
        "men/shoes/hiking"
      ],
      "available": true,
      "url": "https://my-awesome-shop/uk/products/productId1",
      "canonicalUrl": "https://my-awesome-shop/products/productId1",
      "imageUrl": "https://cdn.my-awesome-shop/products/productId1_img1.png",
      "publishedAt": "2019-06-19T23:45:56Z",
      "marketHandle": "uk",
      "markets": ["GB"],
      "groupId": "productId1",
      "variants": [
        {
          "variantId": "variantId1",
          "sku": "TP-001-S-RED",
          "listPrice": 12.5,
          "discountPrice": 10.99,
          "available": true,
          "stockQuantity": 7,
          "imageUrl": "https://cdn.my-awesome-shop/products/productId1_red.png",
          "barcode": "5901234123457",
          "optionValues": [
            {"name": "Size", "value": "S"},
            {"name": "Color", "value": "Red"}
          ]
        },
        {
          "variantId": "variantId2",
          "sku": "TP-001-L-BLUE",
          "listPrice": 14.5,
          "available": false,
          "stockQuantity": 0,
          "imageUrl": "https://cdn.my-awesome-shop/products/productId1_blue.png",
          "optionValues": [
            {"name": "Size", "value": "L"},
            {"name": "Color", "value": "Blue"}
          ]
        }
      ],
      "custom": {
        "string": [
          {"key": "size", "values": ["S", "L"]},
          {"key": "color", "values": ["red", "blue"]}
        ]
      }
    }
  ]
}'
```

The product-level `listPrice`, `discountPrice`, `available`, `stockQuantity`, `sku` and `imageUrl` should describe the variant a customer sees first on the page — normally the first available one. These are the values Releva shows in recommenders, emails and on-site blocks.

### Sending each variant as its own product (legacy)

The older integration style — one Releva product per variant, with the parent product id passed as a `product_id` custom field — is still accepted:

```
curl -H 'Content-Type: application/json' -H 'Authorization: Bearer <secretKey>' -XPOST https://releva.ai/api/v0/products -d'{
  "products": [
    {
      "id": "variantId1",
      "name": "Test Product",
      "description": "Some Description",
      "locale": "en",
      "currency": "USD",
      "listPrice": 12.5,
      "discountPrice": 10.99,
      "categories": [
        "men/shoes/hiking"
      ],
      "available": true,
      "url": "https://my-awesome-shop/products/productId1",
      "imageUrl": "https://cdn.my-awesome-shop/products/productId1_img1.png",
      "publishedAt": "2019-06-19T23:45:56Z",
      "custom": {
        "string": [
          {"key": "product_id", "values": ["productId1"]},
          {"key": "size", "values": ["S"]},
          {"key": "color", "values": ["red"]}
        ]
      }
    }
  ]
}'
```

New integrations should prefer the `variants` array: it keeps one entry per product page in recommenders and search instead of one entry per variant, and it is what the Facebook / Meta catalog sync uses to build variant-level catalog items.

**Note:** the `deleteMissingVariants` flag from earlier versions of this API is deprecated. It is still accepted by the endpoint for backwards compatibility but no longer has any effect. Remove variants you no longer sell from the `variants` array, and remove products you no longer sell with the Product Delete API.

## Selling in several languages, currencies or markets

Releva stores one document per *(product, language, market)* combination, and `id` is what identifies that document. If you sell the same product in more than one language or market, each version needs its own `id`. The convention our own connectors use is:

```
{yourProductId}_{locale}            e.g. 12345_en
{yourProductId}_{locale}_{market}   e.g. 12345_en_uk
```

Set `locale` and `currency` on every document, and for market-scoped catalogs also:

- `marketHandle` — your own identifier for the market (for example `uk`, `europe`). Releva uses it to scope recommendations and on-site search to the market the visitor is browsing.
- `markets` — the ISO-3166-1 alpha-2 country codes the document applies to, in upper case. A market that spans several countries carries them all, e.g. `["FR", "DE", "ES", "IT", "NL"]`. This is what the Facebook / Meta catalog sync uses to publish per-country prices and links.
- `canonicalUrl` — the product's canonical URL without the market or language prefix, while `url` stays the market-specific one. Advertising feeds use `canonicalUrl` as the base link so one market's path prefix is not inherited by the others.
- `groupId` — the same value across all languages and markets of the product, so they are recognised as one product.

## The products Array

This object is an array of Objects with the following properties. Any property that is not listed below is rejected with HTTP 400 — pass extra data in `custom` or `data` instead.

| Field | Type | Description |
| --- | --- | --- |
| `id` | String | A unique product ID identifying your product. It must be unique across all locales *and* markets — see *Selling in several languages, currencies or markets* above. This is also the id used by the Product Delete API. |
| `name` | String | The product name. |
| `description` | String (Optional) | A short description of the product. |
| `locale` | ISO-639-1 String (Optional) | If you sell in multiple languages or currencies, set this to the current locale. Two-letter language codes only (`en`, `bg`) — `en-US` is not valid. If you omit this field, the shop default will be used. |
| `currency` | ISO-4217 String (Optional) | The product currency. If you omit this field, the shop default will be used. |
| `listPrice` | Float | The regular price of the product. |
| `discountPrice` | Float (Optional) | If the product is on sale, set the discount price here. If not, do not include this field. It must be lower than `listPrice`. |
| `stockQuantity` | Integer (Optional) | How many units are still available for sale. Use `0` together with `available: false` for a sold-out product. |
| `sku` | String (Optional) | The SKU of the product (of the primary variant for a multi-variant product). Used by on-site search and, optionally, as the item identifier in advertising catalogs. |
| `categories` | Array[String] | Array of category paths where the product is accessible, e.g. `["men/shoes", "men/shoes/hiking", "sports/hiking/shoes"]`. |
| `available` | Boolean | True if the product is available for sale, false otherwise. |
| `url` | Absolute URL String (Optional) | The product URL, as seen by a visitor of this locale/market. |
| `canonicalUrl` | Absolute URL String (Optional) | The product's canonical URL, without a language or market prefix. Used as the base link in advertising feeds; falls back to `url` when omitted. |
| `imageUrl` | Absolute URL String (Optional) | The URL to the product's image. If omitted, the shop's default product image is used. |
| `publishedAt` | ISO-8601 String (Optional) | The date and time when this product was first available for sale. The time and the timezone offset are required, e.g. `2019-06-19T23:45:56Z`. |
| `marketHandle` | String (Optional) | Your identifier for the market this document belongs to, e.g. `uk`. Used to scope recommendations and search to the visitor's market. |
| `markets` | Array[String] (Optional) | The ISO-3166-1 alpha-2 country codes this document applies to, upper case, e.g. `["FR", "DE"]`. Anything that is not exactly two upper-case letters is rejected. |
| `groupId` | String (Optional) | The id of the product this document belongs to. Use the same value for all variants, languages and markets of one product. Only honoured together with a non-empty `variants` array. |
| `variants` | Array[Object] (Optional) | Per-variant data — see *The variants Array* below. |
| `data` | Object (Optional) | Arbitrary non-searchable product data. Use this to pass through product information that you would like to visualize in recommender results. |
| `custom` | Object (Optional) | The custom fields associated with the product, for example the available sizes — see *The custom Object* below. |
| `inventory` | Array[Object] (Optional) | Product inventory information. |
| `inventory[].location` | Array[Object] (Optional) | Inventory locations in decimal longitude, latitude notation. |
| `inventory[].location[].lon` | Float | Inventory location longitude, e.g. `23.5825895`. |
| `inventory[].location[].lat` | Float | Inventory location latitude, e.g. `39.9352959`. |

## The variants Array

Each entry describes one purchasable variant of the product. Only per-variant data belongs here — name, description, categories and URL stay on the product.

| Field | Type | Description |
| --- | --- | --- |
| `variantId` | String | Your id for this variant. Must be unique within the product. |
| `sku` | String (Optional) | The variant SKU. |
| `listPrice` | Float (Optional) | The regular price of this variant. |
| `discountPrice` | Float (Optional) | The sale price of this variant, if it is on sale. |
| `available` | Boolean (Optional) | True if this variant can be bought. |
| `stockQuantity` | Integer (Optional) | How many units of this variant are still available. |
| `imageUrl` | Absolute URL String (Optional) | The variant image, when it differs from the product image. |
| `barcode` | String (Optional) | The variant barcode / GTIN / EAN. |
| `optionValues` | Array[Object] (Optional) | The variant options, as `{"name": "Size", "value": "S"}` pairs. Send one entry per option axis. |
| `optionValues[].name` | String | The option name, e.g. `Size`. |
| `optionValues[].value` | String | The option value for this variant, e.g. `S`. |

## The custom Object

Custom fields are what you filter, group and personalise on — brand, colour, material, promotion end dates, and anything else specific to your catalog. They are grouped by value type, and each entry is a `key` with an array of `values`.

| Field | Type | Description |
| --- | --- | --- |
| `custom.string` | Array[Object] (Optional) | Text fields, as `{"key": "color", "values": ["red", "blue"]}`. |
| `custom.numeric` | Array[Object] (Optional) | Numeric fields, as `{"key": "size", "values": [14, 15, 18]}`. |
| `custom.date` | Array[Object] (Optional) | Date fields, as `{"key": "in_promo_until", "values": ["2022-11-01T00:00:00.000Z"]}`. A plain `2022-11-01` is accepted here too. |

Releva adds a `category_level_1` … `category_level_8` string field automatically from each of your `categories` paths, so you do not need to send those yourself.

## Sending updates

Each call updates the products it contains and leaves the rest of your catalog untouched. Within a product, always send the **complete** object rather than only the fields that changed:

- `discountPrice` is cleared when it is not included — that is how you take a product off sale.
- `custom` is replaced by what you send; omitting it removes the product's custom fields.
- `variants` is replaced by what you send; omitting it collapses the product back to a single variant.

Price and availability changes are detected by comparing against what Releva already has, and they feed the *price drop*, *back in stock* and *out of stock* triggers — so sending the same payload twice does not re-trigger a campaign.

## Response Format

A successful response will return HTTP Status 202 with an empty response body (`{}`).

An error response will return a HTTP 4xx or 5xx status code and have the following structure:

```
{
  "message": "A description of the error and how to fix it, if it's a client error."
}
```

| Status | Meaning |
| --- | --- |
| 202 | Accepted. The update will be live within 10 seconds. |
| 400 | The request body failed validation, or the `Authorization` header is missing, malformed or does not match a known domain. `message` names the offending field. |
| 402 | The domain is disabled. Please reach out to support@releva.ai to enable it. |
| 413 | The request body is larger than 800 KB. Send the products in smaller batches. |
