> ## Documentation Index
> Fetch the complete documentation index at: https://enrichment-docs.verzla.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Fetching Products

> Pull the catalog as a paginated list, or look up specific SKUs.

There are two ways to read products for enrichment: a **paginated export** of the whole catalog
(ideal for bulk and incremental syncs) and a **targeted SKU lookup** (ideal for re-enriching a
known set). Both require the `catalog:read` scope and both return the same product shape.

## Paginated export

```http theme={null}
GET /api/enrichment/products?page=0&pageSize=100&modifiedSince=<optional>
```

<ParamField query="page" type="integer" default="0">
  Zero-based page index.
</ParamField>

<ParamField query="pageSize" type="integer" default="100">
  Items per page. Clamped to the range **1–500**; values outside it are coerced into range.
</ParamField>

<ParamField query="modifiedSince" type="string">
  Optional. Return only products updated after this moment. Accepts **epoch milliseconds**, an
  **ISO-8601 instant** (`2026-06-01T00:00:00Z`), or a **local date-time** (`2026-06-01T00:00:00`).
  Use it to run incremental syncs instead of re-reading the whole catalog.
</ParamField>

```bash theme={null}
# First page of 250, only products changed since June 1
curl "https://api.verzla.com/api/enrichment/products?page=0&pageSize=250&modifiedSince=2026-06-01T00:00:00Z" \
  -H "X-Api-Client-Key: sk_live_your_key_here"
```

### Paginating through the whole catalog

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function* allProducts(modifiedSince?: string) {
    let page = 0;
    while (true) {
      const qs = new URLSearchParams({ page: String(page), pageSize: "500" });
      if (modifiedSince) qs.set("modifiedSince", modifiedSince);

      const res = await enrichmentFetch(`/products?${qs}`);
      const body = await res.json();
      yield* body.items;

      if (page >= body.totalPages - 1) break;
      page++;
    }
  }

  for await (const product of allProducts()) {
    // enrich(product)
  }
  ```

  ```python Python theme={null}
  def all_products(modified_since=None):
      page = 0
      while True:
          params = {"page": page, "pageSize": 500}
          if modified_since:
              params["modifiedSince"] = modified_since
          body = SESSION.get(f"{BASE}/products", params=params).json()
          yield from body["items"]
          if page >= body["totalPages"] - 1:
              break
          page += 1
  ```
</CodeGroup>

<Info>
  Only **active** products are returned. Results are sorted by ascending `id`, so the ordering is
  stable across pages even while the catalog changes.
</Info>

## Fetch by SKU

When you already know which products to enrich, look them up directly. Send the SKUs in the request
body:

```http theme={null}
POST /api/enrichment/products/skus
Content-Type: application/json
```

```json theme={null}
{
  "skus": ["ABC-123", "ABC-124", "XYZ-900"]
}
```

```bash theme={null}
curl -x POST https://api.verzla.com/api/enrichment/products/skus \
  -H "X-Api-Client-Key: sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "skus": ["ABC-123", "ABC-124", "XYZ-900"] }'
```

<Warning>
  This endpoint accepts **at most 50 SKUs** per call and rejects an **empty** list with
  `400 Bad Request`. Batch larger sets into chunks of 50.
</Warning>

Unknown SKUs are simply absent from the response — there is no per-SKU "not found" entry, so compare
the returned SKUs against what you requested.

## The product shape

Both endpoints return products in the same `EnrichmentProductDto` shape:

```json theme={null}
{
  "id": 90210,
  "sku": "ABC-123",
  "externalId": "erp-55021",
  "gtin": "5012345678900",
  "name": "Cordless Impact Driver",
  "slug": "cordless-impact-driver",
  "description": "An 18V brushless impact driver...",
  "attributeTemplateId": 41,
  "attributes": [
    {
      "attribute": "Voltage",
      "detail": "18",
      "option": {
        "optionId": 880,
        "unit": "V",
        "priority": 1,
        "dataType": "NUMBER",
        "showOnWebsite": true
      }
    }
  ],
  "brand": {
    "id": 12,
    "name": "Acme Tools",
    "website": "https://acme.example",
    "slug": "acme-tools"
  },
  "categories": ["Power Tools", "Drivers"],
  "images": ["https://cdn.verzla.com/p/abc-123/1.jpg"],
  "files": ["https://cdn.verzla.com/p/abc-123/manual.pdf"],
  "variantAttributes": null,
  "variants": null,
  "variantMaster": null
}
```

| Field                             | Type       | Role                                                            |
| --------------------------------- | ---------- | --------------------------------------------------------------- |
| `id`, `sku`, `externalId`, `gtin` | identity   | Match submissions back to this product                          |
| `name`, `slug`, `description`     | enrichable | Propose new values                                              |
| `attributeTemplateId`             | number     | The template this product is bound to                           |
| `attributes`                      | enrichable | Current structured attributes                                   |
| `brand`, `categories`, `images`   | read-only  | Context to feed your model                                      |
| `files`                           | enrichable | Associated document references                                  |
| `variantAttributes`, `variants`   | read-only  | Variant family — populated on the **master** product            |
| `variantMaster`                   | read-only  | Back-reference to the master's SKU — populated on a **variant** |

<Info>
  `attributes` (and each variant's `attributes`) only include values that are actually set — an
  attribute with an empty value is omitted rather than returned as an empty string.
</Info>

## Product variants

Products that belong to a variant family (e.g. the same tool in several voltages) expose their
relationship through three fields. Which are populated depends on whether the product is the family's
**master** or one of its **variants** — the fields that don't apply are `null`.

**Master product** — carries the family. `variantAttributes` lists the attribute names that
distinguish the variants, and `variants` holds the sibling products (the master itself is not
repeated in the array). `variantMaster` is `null`.

```json theme={null}
{
  "sku": "DRILL-18V",
  "name": "Cordless Impact Driver 18V",
  "variantAttributes": ["Voltage"],
  "variants": [
    {
      "id": 90211,
      "sku": "DRILL-12V",
      "name": "Cordless Impact Driver 12V",
      "attributes": [
        { "attribute": "Voltage", "detail": "12", "option": { "optionId": 880, "unit": "V", "priority": 1, "dataType": "NUMBER", "showOnWebsite": true } }
      ]
    }
  ],
  "variantMaster": null
}
```

**Variant product** — points back to its master by SKU via `variantMaster`. `variantAttributes` and
`variants` are `null`.

```json theme={null}
{
  "sku": "DRILL-12V",
  "name": "Cordless Impact Driver 12V",
  "variantAttributes": null,
  "variants": null,
  "variantMaster": "DRILL-18V"
}
```

A product with no variant family has all three fields `null`.

See the full field reference on the [Get products](/api-reference/get-products) page. Next, turn
these into proposals — [Submitting proposals](/enrichment/submitting-proposals).
