> For the complete documentation index, see [llms.txt](https://developer.esw.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.esw.com/customs-catalog-api/resources/tutorials/managing-invalid-products.md).

# Managing Invalid Products

After a catalog upload, the platform validates every product asynchronously. Any product that fails validation or cannot be classified lands in the invalid products queue.&#x20;

***

{% hint style="info" icon="rocket-launch" %}

#### Prerequisites

* A valid Bearer JWT token
* Your `brandCode` or `tenantCode`
* A catalog has been uploaded
  {% endhint %}

{% stepper %}
{% step %}

### Understand Invalid Product Categories

Before querying, it helps to know what the platform considers "invalid". The `searchType` field controls which category of problem you are looking at.

| `searchType`                | What it covers                                                                                                                          |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `InvalidProducts`           | Products that failed one or more validation rules (missing fields, bad values, format errors)                                           |
| `DefaultClassifiedProducts` | Products that could not be matched to an HS classification and have been assigned a default — may still ship, but with reduced accuracy |
| `IncompleteRatings`         | Products that have a classification but are missing duty or tax rating data for one or more destination regions                         |
| `FailedFilesAndApiRequests` | Upload-level failures — the file or API request itself was rejected before products were processed                                      |
| `AwaitingClassification`    | Products accepted and queued, but not yet classified — typically a transient state                                                      |

The `requestType` field provides a second axis of filtering:

| `requestType`           | What it covers                                              |
| ----------------------- | ----------------------------------------------------------- |
| `InvalidProducts`       | Products that failed validation                             |
| `ValidProducts`         | Products that passed validation (useful for cross-checking) |
| `DefaultClassification` | Products assigned a default classification                  |
| `InvalidLogs`           | Error log entries rather than product records               |
| {% endstep %}           |                                                             |

{% step %}

### Get a Dashboard Count

Before fetching product-level detail, get a high-level view of how many products are invalid and how they break down by brand. This is useful for monitoring and triage.

```http
GET /api/v2/Home/TotalInvalidProducts
Authorization: Bearer <token>
```

**Example response:**

```json
{
  "totalInvalidProductCount": 147,
  "invalidProductCountByBrand": [
    { "key": "MBR", "value": 89 },
    { "key": "XYZ", "value": 58 }
  ]
}
```

| Field                        | Description                                       |
| ---------------------------- | ------------------------------------------------- |
| `totalInvalidProductCount`   | Total invalid products across all brands          |
| `invalidProductCountByBrand` | Array of `{ key: brandCode, value: count }` pairs |

If `totalInvalidProductCount` is `0`, there is nothing to action and you can stop here.
{% endstep %}

{% step %}

### List Invalid Products

Use the `POST /api/v2/InvalidProducts/Invalid` endpoint to retrieve the full list of invalid products with their details. The request body is an `InvalidProductSearchRequest` object.

<details>

<summary>Request Body Reference</summary>

| Field               | Type          | Required | Description                                                             |
| ------------------- | ------------- | -------- | ----------------------------------------------------------------------- |
| `pageNumber`        | integer       | ✅        | Page to retrieve (1-based)                                              |
| `pageSize`          | integer       | ✅        | Number of results per page                                              |
| `brandCode`         | string        | ❌        | Filter to a specific brand (three-letter code)                          |
| `tenantCode`        | string        | ❌        | Filter to a specific tenant                                             |
| `searchType`        | string (enum) | ❌        | Category of error — see Step 1                                          |
| `requestType`       | string (enum) | ❌        | Secondary filter type — see Step 1                                      |
| `orderByField`      | string        | ❌        | Field name to sort by                                                   |
| `isOrderByAsc`      | boolean       | ❌        | `true` for ascending, `false` for descending                            |
| `continuationToken` | string        | ❌        | Token from a previous response for large result pagination — see Step 4 |

</details>

#### Basic Example: All Invalid Products for a Brand

{% code expandable="true" %}

```http
POST /api/v2/InvalidProducts/Invalid
Authorization: Bearer <token>
Content-Type: application/json

{
  "pageNumber": 1,
  "pageSize": 50,
  "brandCode": "MBR",
  "searchType": "InvalidProducts",
  "requestType": "InvalidProducts",
  "isOrderByAsc": true
}
```

{% endcode %}

#### Example: Products Awaiting Classification

{% code expandable="true" %}

```http
POST /api/v2/InvalidProducts/Invalid
Authorization: Bearer <token>
Content-Type: application/json

{
  "pageNumber": 1,
  "pageSize": 50,
  "tenantCode": "my-tenant-001",
  "searchType": "AwaitingClassification"
}
```

{% endcode %}

#### Example: Default Classified Products, Sorted by Last Modified

```http
POST /api/v2/InvalidProducts/Invalid
Authorization: Bearer <token>
Content-Type: application/json

{
  "pageNumber": 1,
  "pageSize": 100,
  "tenantCode": "my-tenant-001",
  "searchType": "DefaultClassifiedProducts",
  "orderByField": "lastModifiedDateTime",
  "isOrderByAsc": false
}
```

#### Response Structure

{% code expandable="true" %}

```json
{
  "items": [
    {
      "results": [
        {
          "id": "abc123",
          "brandCode": "MBR",
          "status": "Invalid",
          "dataSource": "Api",
          "dataSourceAdditionalInfo": "catalog-upload-2024-11-01",
          "lastModifiedDateTime": "2024-11-01T14:23:00Z",
          "modifiedBy": "system",
          "correlationId": "corr-xyz-789",
          "userName": "uploader@mybrand.com"
        }
      ],
      "pagination": {
        "currentPage": 1,
        "pageCount": 3,
        "pageSize": 50,
        "rowCount": 147,
        "firstRowOnPage": 1,
        "lastRowOnPage": 50,
        "orderByField": null,
        "isOrderByAsc": true
      }
    }
  ],
  "hasMoreResults": true,
  "continuationToken": "eyJjb250aW51YXRpb24iOiJ0b2tlbi12YWx1ZSJ9"
}
```

{% endcode %}

#### Key Response Fields

| Field                | Description                                                       |
| -------------------- | ----------------------------------------------------------------- |
| `items[].results`    | Array of invalid product records (see below)                      |
| `items[].pagination` | Standard pagination metadata                                      |
| `hasMoreResults`     | `true` if there are more results beyond the current page          |
| `continuationToken`  | Opaque token to fetch the next batch — use in subsequent requests |

<details>

<summary>Product Record Fields</summary>

Each entry in `results` represents one invalid product record:

| Field                      | Description                                                          |
| -------------------------- | -------------------------------------------------------------------- |
| `id`                       | Internal platform ID (Cosmos DB document ID)                         |
| `brandCode`                | Three-letter brand identifier                                        |
| `status`                   | Current product status, e.g. `"Invalid"`, `"AwaitingClassification"` |
| `dataSource`               | How the product was submitted: `"File"` or `"Api"`                   |
| `dataSourceAdditionalInfo` | Additional context — filename for file uploads, blob name for API    |
| `lastModifiedDateTime`     | When the record was last updated (ISO 8601)                          |
| `modifiedBy`               | User or process that last modified the record                        |
| `correlationId`            | Useful for tracing a specific upload event in logs                   |
| `userName`                 | The user who submitted the product                                   |

</details>
{% endstep %}

{% step %}

### Paginate Large Result Sets with Continuation Tokens

For large catalogs, the API uses a **continuation token** pattern on top of standard page numbers. When `hasMoreResults` is `true` in a response, pass the returned `continuationToken` in your next request to fetch the next batch efficiently.

```http
POST /api/v2/InvalidProducts/Invalid
Authorization: Bearer <token>
Content-Type: application/json

{
  "pageNumber": 1,
  "pageSize": 50,
  "tenantCode": "my-tenant-001",
  "searchType": "InvalidProducts",
  "continuationToken": "eyJjb250aW51YXRpb24iOiJ0b2tlbi12YWx1ZSJ9"
}
```

> **Important:** Keep `pageNumber` as `1` when using a `continuationToken` — the token already encodes the position. Incrementing `pageNumber` alongside a token may produce unexpected results.

{% code title="Pseudocode for iterating all results" %}

```ruby
token = null
allProducts = []

repeat:
  body = { pageNumber: 1, pageSize: 100, tenantCode: "...", continuationToken: token }
  response = POST /InvalidProducts/Invalid with body
  allProducts += response.items[0].results
  token = response.continuationToken
until response.hasMoreResults == false
```

{% endcode %}
{% endstep %}

{% step %}

### Download an Error Report File

Rather than paginating through results programmatically, you can download a file containing all invalid products for offline review, sharing with a team, or importing into a spreadsheet.

{% hint style="info" icon="a" %}

#### Option A — Download the Latest Generated File

Retrieves the most recently generated error file for a tenant and search type:

{% code expandable="true" %}

```http
GET /api/v2/InvalidProducts/Invalid/File/{tenantCode}/{searchType}
Authorization: Bearer <token>
```

{% endcode %}

**Example:**

{% code expandable="true" %}

```http
GET /api/v2/InvalidProducts/Invalid/File/my-tenant-001/InvalidProducts
Authorization: Bearer <token>
```

{% endcode %}

**Supported `searchType` path values:** `InvalidProducts` · `DefaultClassifiedProducts` · `IncompleteRatings` · `FailedFilesAndApiRequests` · `AwaitingClassification`
{% endhint %}

{% hint style="info" icon="b" %}

#### Option B — Generate a Fresh File Then Download

If you want to ensure the file reflects the current state of the platform rather than a cached version, use the `POST` version first to generate a new file, then download it with `GET`.

{% endhint %}

{% @code-walkthrough/tutorial-widget src="<https://gist.githubusercontent.com/esw-docs/3ea72b8ed2d9934ea77a9f9b769d4d3b/raw/2b7b2096f5435de1b5e2634695780bf5116771d0/catalogtut2.md>" title="Tutorial" stepCount="3" height="medium" %}
{% endstep %}

{% step %}

### Trigger Reprocessing

Once you have corrected the underlying data issues — either by fixing your source catalog and re-uploading, or by updating classification data — trigger reprocessing to run the invalid products back through the validation and classification pipeline.

#### Reprocess by Brand Code

```http
POST /api/v2/InvalidProducts/Reprocess?brandCode=MBR
Authorization: Bearer <token>
```

#### Reprocess by Tenant Code

```http
POST /api/v2/InvalidProducts/Reprocess/TenantCode?tenantCode=my-tenant-001
Authorization: Bearer <token>
```

Both return `202 Accepted`, meaning reprocessing has been queued — not that it has completed. Wait a reasonable interval and then repeat Step 2 to check whether the invalid count has decreased.

> **Tip:** If the invalid count does not decrease after reprocessing, the root cause in the product data has likely not been resolved. Review the error report again before reprocessing a second time.
> {% endstep %}

{% step %}

### Bulk Delete Stale Invalid Records

If invalid products are genuinely stale — for example, products that have been discontinued and will never be corrected — you can delete them from the platform in bulk using the same `InvalidProductSearchRequest` structure used for listing.

> ⚠️ **This operation is not reversible.** Deleted records cannot be recovered. You must re-upload a product to get it back into the platform.

```http
POST /api/v2/InvalidProducts/Delete
Authorization: Bearer <token>
Content-Type: application/json

{
  "pageNumber": 1,
  "pageSize": 1000,
  "brandCode": "MBR",
  "searchType": "InvalidProducts",
  "requestType": "InvalidProducts"
}
```

Responses:

* `200 OK` — records deleted
* `400 Bad Request` — invalid request body
* `404 Not Found` — no matching records found

The delete endpoint uses the same filtering as the list endpoint — be precise with your `brandCode`, `tenantCode`, `searchType`, and `requestType` to avoid deleting records you intended to keep.
{% endstep %}

{% step %}

### Build a Monitoring Loop

For ongoing catalog health monitoring, you can poll the dashboard endpoint on a schedule and alert when the invalid count rises above a threshold.

**Recommended polling pattern:**

```ruby
every 15 minutes:
  response = GET /api/v2/Home/TotalInvalidProducts

  if response.totalInvalidProductCount > THRESHOLD:
    alert("Invalid products detected")
    for each brand in response.invalidProductCountByBrand:
      if brand.value > 0:
        log(brand.key, brand.value)
        trigger detailed review for brand.key
```

**Useful thresholds to consider:**

| Threshold type                                 | Suggested action                                       |
| ---------------------------------------------- | ------------------------------------------------------ |
| Count > 0 after a new upload                   | Fetch and log error detail per brand                   |
| Count unchanged after reprocessing             | Flag for manual review — fix may not have been applied |
| `FailedFilesAndApiRequests` > 0                | Investigate upload pipeline — not a product data issue |
| `AwaitingClassification` persists for > 1 hour | May indicate a classification service delay            |
| {% endstep %}                                  |                                                        |
| {% endstepper %}                               |                                                        |

### End-to-End Flow Summary

{% code expandable="true" %}

```shellscript
GET /Home/TotalInvalidProducts
  │
  ├── Count = 0 ✅ Nothing to do
  │
  └── Count > 0 ↓
        │
        ▼
POST /InvalidProducts/Invalid
  → Review per-product error detail
  → Paginate with continuationToken if hasMoreResults = true
        │
        ├── Want a file report?
        │     POST /InvalidProducts/Invalid/File/{tenantCode}/{searchType}  ← generate
        │     GET  /InvalidProducts/Invalid/File/{tenantCode}/{searchType}  ← download
        │
        ├── Can fix data? ──→ Fix source → re-upload catalog (Tutorial 3)
        │                        └──→ POST /InvalidProducts/Reprocess
        │
        ├── Already in platform, just needs re-run?
        │     └──→ POST /InvalidProducts/Reprocess  or  /Reprocess/TenantCode
        │
        └── Products are discontinued / stale?
              └──→ POST /InvalidProducts/Delete
                        │
                        ▼
              GET /Home/TotalInvalidProducts  ← verify count reduced
```

{% endcode %}

***

### Common Issues

| Symptom                                                      | Likely Cause                                     | Action                                                                                |
| ------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `totalInvalidProductCount` stays the same after reprocessing | Underlying data error not fixed                  | Review the error report again; correct the product data and re-upload                 |
| `204 No Content` from file generation                        | No invalid products match the given `searchType` | Try a different `searchType`, or confirm products have been processed                 |
| `404` on delete                                              | No products match the filter                     | Check `brandCode`/`tenantCode` spelling and `searchType` combination                  |
| `AwaitingClassification` count does not decrease             | Classification pipeline may be delayed           | Wait and poll again; escalate if count persists beyond a few hours                    |
| `FailedFilesAndApiRequests` count > 0                        | Upload itself was rejected                       | Check the error report for file-level errors; correct format or authentication issues |
| High `DefaultClassifiedProducts` count                       | Products lack `hsCode` or `category`             | Enrich your product data with HS codes and categories — see Tutorial 5                |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developer.esw.com/customs-catalog-api/resources/tutorials/managing-invalid-products.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
