# Catalog sync

> How to export BILLZ products to an external system and keep them up to date.

Task: an external system (online store, marketplace, BI) must hold a copy of the BILLZ catalog that is updated without re-reading everything.

## Initial load

```bash
curl -X GET 'https://api-admin.billz.ai/v2/products?limit=100&page=1' \
  -H 'Authorization: Bearer TOKEN'
```

Pages are iterated until the whole `count` has been fetched. Page size: 100–200 records.

Hard limits of the method; exceeding them returns `400`:

| Parameter | Maximum        |
| --------- | -------------- |
| `limit`   | 300            |
| `page`    | 10 000         |
| `search`  | 100 characters |

## Incremental update

After that, only changes are requested:

```bash
curl -X GET 'https://api-admin.billz.ai/v2/products?last_updated_date=2026-08-14%2000:00:00&limit=100' \
  -H 'Authorization: Bearer TOKEN'
```

> **Time in UTC**
>
> `last_updated_date` is in UTC; the last-sync timestamp is stored in UTC.
>
> Verify the offset before going live: change a product, call the method with the timestamp “moment of change minus one minute” in UTC and make sure the product is returned.

Cycle: remember the **start** time of the request → export the changes → on success store that time as the new timestamp.

## Two storefront sync modes

The storefront is kept up to date by **two** schedules on top of the same `GET /v2/products`.

| Mode      | How often            | What it does                                                                       | What it cannot do                               |
| --------- | -------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Delta** | usually once an hour | fetches changes since the previous sync and updates only the products that arrived | does not notice products that disappeared       |
| **Full**  | once a day           | brings the storefront catalog to the state that came in the response               | more expensive in requests, cannot be run often |

### Delta

A request with `last_updated_date`, as in the section above. Only the products from the response are updated.

### Full

Once a day the whole catalog is exported without `last_updated_date`, and the storefront is brought to the state of the response. The rule for every storefront product:

| In the storefront catalog | In the API response | Has stock | What to do                                                       |
| ------------------------- | ------------------- | --------- | ---------------------------------------------------------------- |
| yes                       | yes                 | yes       | show                                                             |
| yes                       | yes                 | no        | delist or show “out of stock”, depending on the platform’s logic |
| yes                       | **no**              | —         | **delist**                                                       |
| no                        | yes                 | yes       | create the card and show                                         |

> **A missing product is delisted**
>
> Deleting a card on marketplaces is irreversible and loses history, reviews and rating; a delisted card is restored when the product comes back.

> **The result is applied only after a complete export**
>
> An interrupted export would delist the whole part of the catalog that was not exported. The result is applied only when **all** pages have been exported and the count has been checked against the previous export.

## Complex selection

When filters by categories, brands, suppliers or price ranges are needed, use `POST /v2/product-search-with-filters`. Differences from `/v2/products`:

* the stock structure is different: `total_active_measurement_value` instead of `active_measurement_value`;
* there is cursor pagination: every product comes with a `sort` field, which is passed in the next request.

```bash
curl -X POST 'https://api-admin.billz.ai/v2/product-search-with-filters' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "status": "all",
    "brand_ids": ["e158f882-d0e0-499d-b6f8-9029ac1886ca"],
    "shop_ids": ["6f244db4-75d6-4fac-9841-14867d45e36a"],
    "group_variations": false,
    "limit": 50,
    "page": 1
  }'
```

Filtering by attributes and custom properties goes through a single `product_field_filters` parameter, where `field_type` is `attribute` or `custom`:

```json
{
  "product_field_filters": [
    { "field_id": "491e1d8a-...", "field_type": "attribute", "field_values": ["aee6d71d-..."] },
    { "field_id": "3b6db763-...", "field_type": "custom", "field_values": ["18"] }
  ]
}
```

## Products with variations

With `"group_variations": true` the list returns the parent product, and the variations are in the `variations` field.

## Reference data

Categories, brands, units of measure, characteristics and product types are exported by separate methods: `/v2/category`, `/v2/brand`, `/v2/measurement-unit`, `/v2/product-characteristic`, `/v2/product-type`. They are refreshed once a day.

> **Attributes come in readable form**
>
> In `product_attributes[]`, `attribute_name` and `attribute_value` come as strings next to the identifiers. Attribute reference data is needed only to build filters.

Each reference method has its own response shape: `{categories, count}`, `{brands, count}`, `{measurement_units, count}`; characteristics have `active_count` and `deleted_count` instead of `count`, and `/v2/product-type` returns `{product_types}` without a counter and without pagination.

## Request rate

The API accepts up to **2 requests per second**; exceeding this returns `429`, which is handled by retrying with exponential backoff. The limit is counted over the integration’s total traffic.

## What not to do

* Do not serve product photos directly from the BILLZ CDN; cache media on the integration side.