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

# Catalog

> The shared SKU catalog - keep it in step with an ERP or PIM, check the codes in use against the active tariff schedule, and read the alerts raised when a schedule changes.

The catalog holds your products with the tariff code each one uses. It answers two questions an ERP or PIM
integration has: is the code we use still a valid line of the current schedule, and which products are affected
when the schedule changes.

A SKU is identified by `sku` and `market` together: the same `sku` can exist once for `US`, once for `EU` and once for `VN`.
Creating a SKU that exists updates it, so a sync can be repeated safely.

When the free trial of the account has ended, the account is read-only: reading the catalog and the alerts still
works, and every write on this page (create, sync, replace, delete, classify, check the codes, acknowledge an alert)
is answered `402` with the code `trial_ended`, before the fields of the request are checked and before the SKU is
looked up ([Errors](/errors.md#a-read-only-account)). Classifying a SKU can also answer `402` with `out_of_credits`.

## The SKU object

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | integer | The id of the SKU in the catalog |
| `sku` | string | Your SKU code, up to 128 characters |
| `market` | string | `US`, `EU` or `VN` |
| `description`, `material`, `use`, `composition`, `origin`, `notes` | string | The product, as on a lookup |
| `origin_label` | string | The country of origin, named in the language of the reader |
| `external_ref` | string | Your own reference, for example the id in the ERP. Stored and returned as sent |
| `current_code` | string or null | The code in use |
| `code_status` | string | `valid` (a declarable line of the active schedule), `not_declarable` (a heading or a line that is not declarable), `invalid` (not in the schedule), `changed` or `expired` (affected by a new version), or `unknown` (no code) |
| `code_status_label`, `code_status_detail` | string | The status in words |
| `checked_version`, `checked_at` | string or null | The tariff version the code was checked against, and when |
| `suggested_code` | string or null | The code of the latest lookup for this SKU |
| `last_lookup_id` | string or null | That lookup |
| `created_at`, `updated_at` | string | ISO 8601, UTC |
| `alerts` | array | The latest 50 alerts of the SKU, acknowledged or not. Only on `GET /api/skus/{sku_id}` |

## The alert object

An alert is raised for a SKU when a new version of the tariff schedule removes or changes its code. Each alert
has an `id`, the `sku_id`, a `kind` (`code_removed`, `code_changed` or `code_invalid`), a `message`, `created_at`, and
`acknowledged_at` with `acknowledged_by` once someone has dealt with it. Raising one sends the webhook event `sku.alert` ([Webhooks](/webhooks.md)).

## Create or update a SKU

`POST /api/skus`

**Changes data in your account when executed.** A SKU is identified by `sku` and `market`. `current_code`, when given, is checked against the active tariff schedule of the market.

**Request body** (`application/json`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `sku` | string | Yes | Up to 128 characters |
| `market` | string | No | Up to 8 characters, default `"US"` |
| `description` | string | Yes | Up to 2000 characters |
| `material` | string | No | Up to 500 characters |
| `use` | string | No | Up to 500 characters |
| `composition` | string | No | Up to 500 characters |
| `origin` | string | No | Up to 100 characters |
| `notes` | string | No | Up to 1000 characters |
| `external_ref` | string | No | Up to 255 characters |
| `current_code` | string or null | No | Up to 24 characters |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `402` The free trial of the account has ended and the account is read-only: `trial_ended`; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl -X POST "https://htspilot.com/api/skus" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
  "sku": "EXAMPLE-TSH-001",
  "market": "US",
  "description": "Men'\''s T-shirt, knitted, 100% cotton, short sleeves",
  "material": "cotton",
  "use": "apparel",
  "composition": "100% cotton",
  "origin": "VN",
  "notes": "",
  "external_ref": "ERP-100045",
  "current_code": "6109.10.00.12"
}'
```

**Example response** `200`

```json
{
  "id": 1,
  "sku": "EXAMPLE-TSH-001",
  "market": "US",
  "description": "Men's T-shirt, knitted, 100% cotton, short sleeves",
  "material": "cotton",
  "use": "apparel",
  "composition": "100% cotton",
  "origin": "VN",
  "origin_label": "Vietnam (VN)",
  "notes": "",
  "external_ref": "ERP-100045",
  "current_code": "6109.10.00.12",
  "code_status": "valid",
  "code_status_label": "Valid",
  "code_status_detail": "Valid declarable code",
  "checked_version": "2026-SAMPLE",
  "checked_at": "2026-10-04T20:23:39.053317+00:00",
  "suggested_code": null,
  "last_lookup_id": null,
  "created_at": "2026-10-04T20:23:39.052315+00:00",
  "updated_at": "2026-10-04T20:23:39.052315+00:00"
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/createSku

## Sync many SKUs

`POST /api/skus/bulk`

**Changes data in your account when executed.** Up to 5000 items per call. With `check_codes` every `current_code` is checked against the active tariff schedule. With `classify_missing` the SKUs without a code are classified in the background, which spends credits.

**Request body** (`application/json`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | SkuIn[] | Yes | - |
| `check_codes` | boolean | No | Default `true` |
| `classify_missing` | boolean | No | Classify SKUs without a code (in the background). Default `false` |

**`items`**: SkuIn

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `sku` | string | Yes | Up to 128 characters |
| `market` | string | No | Up to 8 characters, default `"US"` |
| `description` | string | Yes | Up to 2000 characters |
| `material` | string | No | Up to 500 characters |
| `use` | string | No | Up to 500 characters |
| `composition` | string | No | Up to 500 characters |
| `origin` | string | No | Up to 100 characters |
| `notes` | string | No | Up to 1000 characters |
| `external_ref` | string | No | Up to 255 characters |
| `current_code` | string or null | No | Up to 24 characters |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `402` The free trial of the account has ended and the account is read-only: `trial_ended`; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl -X POST "https://htspilot.com/api/skus/bulk" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "sku": "EXAMPLE-TSH-001",
      "market": "US",
      "description": "Men'\''s T-shirt, knitted, 100% cotton, short sleeves",
      "material": "cotton",
      "use": "apparel",
      "composition": "100% cotton",
      "origin": "VN",
      "notes": "",
      "external_ref": "ERP-100045",
      "current_code": "6109.10.00.12"
    },
    {
      "sku": "EXAMPLE-TWL-014",
      "market": "US",
      "description": "Cotton terry bath towel, 70 x 140 cm",
      "origin": "IN",
      "external_ref": "ERP-100046"
    }
  ],
  "check_codes": true,
  "classify_missing": false
}'
```

**Example response** `200`

```json
{
  "upserted": 2,
  "classification_queued": 0,
  "examples_skipped": 0,
  "items": [
    {
      "id": 1,
      "sku": "EXAMPLE-TSH-001",
      "market": "US",
      "code_status": "valid"
    },
    {
      "id": 2,
      "sku": "EXAMPLE-TWL-014",
      "market": "US",
      "code_status": "unknown"
    }
  ]
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/syncSkus

The answer lists each item with its `id` and `code_status`. `classify_missing` creates a lookup for every SKU
without a code: the lookups can be charged, the call needs the plan feature `feature.batch`, and the whole call is refused when
the count is over the plan's row limit. The call itself never answers `out_of_credits`: a lookup that cannot be
charged is not created, and its SKU stays without a suggested code. The suggested codes arrive as the lookups finish: read
them from the SKUs (`suggested_code`, `last_lookup_id`). These lookups send no `lookup.completed` event.

One call carries at most `max_sku_bulk_items` items, 5000 by default: read the number in force from
`GET /api/limits` ([System](/system.md#request-limits)) and send a larger catalog in several calls. A call with
more items answers `422` and nothing is saved. This call takes JSON and is not a file upload: the `413` for a
file that is too large, the `429` of an upload that finds the service busy, the `408` of a file that arrives too
slowly and the `415` of a body that is not a form do not apply to it. To send a file
of products, use a batch ([Batches](/batches.md)).

## List SKUs

`GET /api/skus`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `q` | string or null | No | Up to 200 characters |
| `market` | string or null | No | - |
| `code_status` | string or null | No | - |
| `has_code` | boolean or null | No | True: only SKUs with an existing code; false: only without |
| `has_alert` | boolean or null | No | True: only SKUs with an open alert; false: only without |
| `page` | integer | No | Minimum 1, default `1` |
| `page_size` | integer | No | Minimum 1, maximum 500, default `50` |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl "https://htspilot.com/api/skus?page=1&page_size=50" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME"
```

**Example response** `200`

```json
{
  "items": [
    {
      "id": 2,
      "sku": "EXAMPLE-TWL-014",
      "market": "US",
      "description": "Cotton terry bath towel, 70 x 140 cm",
      "material": "",
      "use": "",
      "composition": "",
      "origin": "IN",
      "origin_label": "India (IN)",
      "notes": "",
      "external_ref": "ERP-100046",
      "current_code": null,
      "code_status": "unknown",
      "code_status_label": "No code",
      "code_status_detail": "",
      "checked_version": "",
      "checked_at": null,
      "suggested_code": null,
      "last_lookup_id": null,
      "created_at": "2026-10-04T20:23:39.058832",
      "updated_at": "2026-10-04T20:23:39.058832",
      "open_alerts": 0
    },
    {
      "id": 1,
      "sku": "EXAMPLE-TSH-001",
      "market": "US",
      "description": "Men's T-shirt, knitted, 100% cotton, short sleeves",
      "material": "cotton",
      "use": "apparel",
      "composition": "100% cotton",
      "origin": "VN",
      "origin_label": "Vietnam (VN)",
      "notes": "",
      "external_ref": "ERP-100045",
      "current_code": "6109.10.00.12",
      "code_status": "valid",
      "code_status_label": "Valid",
      "code_status_detail": "Valid declarable code",
      "checked_version": "2026-SAMPLE",
      "checked_at": "2026-10-04T20:23:39.057328",
      "suggested_code": null,
      "last_lookup_id": null,
      "created_at": "2026-10-04T20:23:39.052315",
      "updated_at": "2026-10-04T20:23:39.058330",
      "open_alerts": 0
    }
  ],
  "total": 2,
  "page": 1,
  "page_size": 50,
  "counts": {
    "unknown": 1,
    "valid": 1
  }
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/listSkus

## Get a SKU

`GET /api/skus/{sku_id}`

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `sku_id` | integer | - |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl "https://htspilot.com/api/skus/1" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME"
```

**Example response** `200`

```json
{
  "id": 1,
  "sku": "EXAMPLE-TSH-001",
  "market": "US",
  "description": "Men's T-shirt, knitted, 100% cotton, short sleeves",
  "material": "cotton",
  "use": "apparel",
  "composition": "100% cotton",
  "origin": "VN",
  "origin_label": "Vietnam (VN)",
  "notes": "",
  "external_ref": "ERP-100045",
  "current_code": "6109.10.00.12",
  "code_status": "valid",
  "code_status_label": "Valid",
  "code_status_detail": "Valid declarable code",
  "checked_version": "2026-SAMPLE",
  "checked_at": "2026-10-04T20:23:39.057328",
  "suggested_code": null,
  "last_lookup_id": null,
  "created_at": "2026-10-04T20:23:39.052315",
  "updated_at": "2026-10-04T20:23:39.058330",
  "alerts": []
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/getSku

## Replace a SKU

`PUT /api/skus/{sku_id}`

**Changes data in your account when executed.** `sku` and `market` identify the SKU and are not changed.

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `sku_id` | integer | - |

**Request body** (`application/json`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `sku` | string | Yes | Up to 128 characters |
| `market` | string | No | Up to 8 characters, default `"US"` |
| `description` | string | Yes | Up to 2000 characters |
| `material` | string | No | Up to 500 characters |
| `use` | string | No | Up to 500 characters |
| `composition` | string | No | Up to 500 characters |
| `origin` | string | No | Up to 100 characters |
| `notes` | string | No | Up to 1000 characters |
| `external_ref` | string | No | Up to 255 characters |
| `current_code` | string or null | No | Up to 24 characters |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `402` The free trial of the account has ended and the account is read-only: `trial_ended`; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl -X PUT "https://htspilot.com/api/skus/1" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
  "sku": "EXAMPLE-TSH-001",
  "market": "US",
  "description": "Men'\''s T-shirt, knitted, 100% cotton, short sleeves",
  "material": "cotton",
  "use": "apparel",
  "composition": "100% cotton",
  "origin": "VN",
  "notes": "",
  "external_ref": "ERP-100045",
  "current_code": "6109.10.00.12"
}'
```

**Example response** `200`

```json
{
  "id": 1,
  "sku": "EXAMPLE-TSH-001",
  "market": "US",
  "description": "Men's T-shirt, knitted, 100% cotton, short sleeves",
  "material": "cotton",
  "use": "apparel",
  "composition": "100% cotton",
  "origin": "VN",
  "origin_label": "Vietnam (VN)",
  "notes": "",
  "external_ref": "ERP-100045",
  "current_code": "6109.10.00.12",
  "code_status": "valid",
  "code_status_label": "Valid",
  "code_status_detail": "Valid declarable code",
  "checked_version": "2026-SAMPLE",
  "checked_at": "2026-10-04T20:23:39.069891+00:00",
  "suggested_code": null,
  "last_lookup_id": null,
  "created_at": "2026-10-04T20:23:39.052315",
  "updated_at": "2026-10-04T20:23:39.058330"
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/updateSku

## Delete a SKU

`DELETE /api/skus/{sku_id}`

**Deletes for real when executed, and cannot be undone.** Needs the reviewer or the admin role.

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `sku_id` | integer | - |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `402` The free trial of the account has ended and the account is read-only: `trial_ended`; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl -X DELETE "https://htspilot.com/api/skus/1" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME"
```

**Example response** `200`

```json
{
  "ok": true
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/deleteSku

## Classify a SKU

`POST /api/skus/{sku_id}/classify`

**Changes data in your account when executed.** Runs a lookup for the SKU and stores the result as its suggested code. Spends credits.

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `sku_id` | integer | - |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `402` The free trial of the account has ended and the account is read-only: `trial_ended`. The credits of the account that is charged do not cover the lookups of the call: `out_of_credits`; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl -X POST "https://htspilot.com/api/skus/1/classify" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME"
```

**Example response** `200`

```json
{
  "lookup_id": "668568f3391548c489131e060fcd6ee3",
  "status": "proposed",
  "recommended_code": "6109.10.00.14",
  "confidence": 0.75
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/classifySku

Unlike `POST /api/classify`, this call waits for the result and answers with the outcome. The lookup has the kind
`sku` and sends no `lookup.completed` event.

## Check the codes in use

`POST /api/skus/check`

**Changes data in your account when executed.** Checks every SKU that has a `current_code`, or only those of one market or the ones listed in `sku_ids`.

**Request body** (`application/json`)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `market` | string or null | No | - |
| `sku_ids` | integer[] or null | No | - |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `402` The free trial of the account has ended and the account is read-only: `trial_ended`; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl -X POST "https://htspilot.com/api/skus/check" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{
  "market": "US"
}'
```

**Example response** `200`

```json
{
  "checked": 1,
  "counts": {
    "valid": 1
  }
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/checkSkuCodes

## Download the import template

`GET /api/skus/template`

An .xlsx file with the import columns and their lists: the countries of origin and the markets that are loaded.

**Returns** `200` with a file (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`).

**Errors**: `401` No valid API key was sent; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl "https://htspilot.com/api/skus/template" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME" \
  -o "sku_catalog_template.xlsx"
```

**Example response** `200`

```json
{
  "file": true,
  "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  "content_disposition": "attachment; filename=\"sku_catalog_template.xlsx\"",
  "bytes": 11660
}
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/getSkuTemplate

## List alerts

`GET /api/alerts`

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `open_only` | boolean | No | Default `true` |
| `sku_id` | integer or null | No | - |
| `limit` | integer | No | Minimum 1, maximum 1000, default `200` |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl "https://htspilot.com/api/alerts" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME"
```

**Example response** `200`

```json
[]
```

Try it: https://docs.htspilot.com/explorer/#/Catalog/listAlerts

## Acknowledge an alert

`POST /api/alerts/{alert_id}/ack`

**Changes data in your account when executed.** Needs the reviewer or the admin role.

**Path parameters**

| Name | Type | Description |
| --- | --- | --- |
| `alert_id` | integer | - |

**Returns** `200` with a JSON object, as in the example.

**Errors**: `401` No valid API key was sent; `402` The free trial of the account has ended and the account is read-only: `trial_ended`; `403` The role of the key, or the plan of the account, does not allow this call; `422` The request is not valid; `500` An unexpected failure.

**Example request**

```bash
curl -X POST "https://htspilot.com/api/alerts/1/ack" \
  -H "X-API-Key: hts_EXAMPLE_KEY_REPLACE_ME"
```

No example answer was captured for this operation.

Try it: https://docs.htspilot.com/explorer/#/Catalog/acknowledgeAlert
